1. Clojure Data Type Diagram

다음의 도표는 클로저의 모든 자료형들을 한 눈에 살펴볼 수 있도록 일목요연하게 정리한 것이다. 먼저 전체적인 지도를 마음 속에 두고 각 부분을 자세하게 살펴 나가는 것이 클로저를 빠르고 정확하게 이해하는 지름길이다. 각 자료형 옆에는 실제로 구현되어 있는 자바 클래스를 병기했다. 이 장에서는 단순 자료형(Simple values)들을 중심으로 살펴보고, 다음 장에서 복합 자료형인 Collection들을 살펴 보겠다.

클로저 타입 자바 클래스

Immutables (= Values)

Simple values

Integers

java.lang.Long clojure.lang.BigInt

Floats

java.lang.Double
java.math.BigDecimal

Ratios

clojure.lang.Ratio

Strings

java.lang.String

Characters

java.lang.Character

Symbols

clojure.lang.Symbol

Keywords

clojure.lang.Keyword

Booleans

java.lang.Boolean

nil

null

Collections

Lists

clojure.lang.PersistentList

Vectors

clojure.lang.PersistentVector

Maps

array-map: clojure.lang.PersistentArrayMap hash-map: clojure.lang.PersistentHashMap sorted-map: clojure.lang.PersistentTreeMap

Sets

hash-set: clojure.lang.PersistentHashSet sorted-set: clojure.lang.PersistentTreeSet

Mutables (= References)

Identities

Atoms

clojure.lang.Atom

Refs

clojure.lang.Ref

Agents

clojure.lang.Agent

Vars

clojure.lang.Var

2. 숫자(Numbers)

2.1. 정수(Integers)

클로저에서 정수는 자기 자신으로 평가되는 값(value)이다. 즉, 아래에서 정수 17을 평가하면 그 결과는 자기 자신인 17이 된다.

17   ;=> 17

type 함수를 이용하면, 해당 값이 어떤 자료형으로 구현되어 있는지를 직접 확인해 볼 수 있다.

(type 17)   ;=> java.lang.Long

위의 결과에서도 알 수 있듯이, 클로저에서 정수형은 자바의 '기본(primitive) 자료형 long' 타입을 내부적으로 감싸고 있는 wrapper class인 java.lang.Long 클래스의 인스턴스이다.

자바의 기본(primitive) 자료형인 long형이 아니라, 레퍼런스(reference) 타입인 java.lang.Long형임에 주의하자. 레퍼런스 타입이므로 컬렉션 자료형의 요소로 쓰일 수 있다.
클로저에서는 정수 연산의 속도를 높이기 위한 방법으로, 기본 자료형인 long형도 다룰 수 있는 방법을 제공한다. 이에 대해서는 Numerics and Mathematics 장에서 자세히 다룬다.

정수 맨앞에 숫자 0이 있으면 8진수를 표시하고, 0x 또는 0X가 있으면 16진수를 표시한다.

010    ;=> 8

0x10   ;=> 16
0X10   ;=> 16

클로저는 8진수와 16진수 이외에도, 2진수에서 36진수까지 표기할 수 있는 방법을 제공한다. r 또는 R 문자[1] 앞에 표현하고자 하는 진법을 나타내고, 그 뒤에 숫자를 표시하면 된다. 실제 숫자를 표시할 때, 0 — 9를 넘어가는 숫자는 알파벳 a — z 또는 A — Z 문자를 이용한다.

2 ~ 36진수까지의 표현은 java.lang.Long.parseLong을 이용한다.
;; 2진법
2r1111   ;=> 15

;; 3진법
3r11     ;=> 4

;; 8진법
8r11     ;=> 9

;; 16진법
16rFF    ;=> 255

;; 36진법
36r1z    ;=> 71

정수의 맨 뒤에 대문자 N[2]을 붙이면, Long형보다 큰 임의의 길이의 정수(arbitrary-precision integer)도 표현할 수 있다.

99999999999999999999N   ;=> 99999999999999999999N

다음의 2Njava.lang.Long형이 아니라, clojure.lang.BigInt형이다.

2N          ;=> 2N

(type 2N)   ;=> clojure.lang.BigInt

Long형보다 큰 정수에 명시적으로 N을 붙이지 않아도, clojure.lang.BigInt형으로 자동 형변환된다.

 99999999999999999999   ;=> 99999999999999999999N

자동 승격 연산자(+', -', *', inc', and dec')들은 Long 계산이 오버플로우가 발생하면 자동으로 BigInt로 변환해 준다.

(+ 1 Long/MAX_VALUE)
;>> java.lang.ArithmeticException: integer overflow

(+' 1 Long/MAX_VALUE)
;=> 9223372036854775808N

(class (+' 1 Long/MAX_VALUE))
;=> clojure.lang.BigInt

(class (+' 1))
;=> java.lang.Long

bigint 함수는 BigInt를 만든다.

(bigint 1))     ;=> 1N

biginteger 함수는 java.math.BigInteger를 만든다.

(biginteger 1)          ;=> 1
(type (biginteger 1))   ;=> java.math.BigInteger

2.2. 실수(Floats)

클로저에서 부동 소수점 실수 역시 자기 자신으로 평가되는 값(value)이다. 클로저에서 실수형은 자바의 '기본(primitive) 자료형 double' 타입을 내부적으로 감싸고 있는 wrapper class인 java.lang.Double 클래스의 인스턴스이다.

클로저에서는 실수 연산의 속도를 높이기 위한 방법으로, 기본 자료형인 double형도 다룰 수 있는 방법을 제공한다. 이에 대해서는 Numerics and Mathematics 장에서 자세히 다룬다.
3.14          ;=> 3.14

(type 3.14)   ;=> java.lang.Double

과학적 표기법(Scientific Notation)도 지원한다.

6.0221412927e23   ;=> 6.0221412927E23

실수 뒤에 대문자 M[3]을 붙이면, 정확도가 보장되는 임의의 길이의 실수(arbitrary-precision signed floating point decimal)를 표현할 수 있다.

100.01M           ;=> 100.01M

(type 100.01M)    ;=> java.math.BigDecimal

(* 100.01M 100)   ;=> 10001.00M

decimal? 함수는 주어진 숫자가 BigDecimal형인지 판별한다.

(decimal? 0.1M)   ;=> true

(decimal? 0.1)    ;=> false
(decimal? 1)      ;=> false

bigdec 함수는 BigDecimal을 만든다.

(bigdec 3.0)    ;=> 3.0M

2.3. 분수 (Ratios)

클로저에서는 분수 자료형도 제공한다. 분자와 분모 사이에 슬래시(/) 기호를 사용해 표현한다. 이때 분자, 분모와 슬래시 기호 사이에는 공백이 없어야 한다.

1/3          ;=> 1/3
7/4          ;=> 7/4

(type 2/3)   ;=> clojure.lang.Ratio

ratio? 함수는 주어진 숫자가 clojure.lang.Ratio형인지를 판별한다.

(ratio? 4/7)   ;=> true
(ratio? 7)     ;=> false

약분이 가능한 경우에는, 약분된 값이 반환된다.

2/4   ;=> 1/2

분자와 분모는 모두 정수형이어야 한다. 그렇지 않으면 예외가 발생한다.

2/3.5
;>> NumberFormatException Invalid number: 2/3.5

denominator 함수와 numerator 함수는 인수로 주어진 분수의 분자와 분모 부분만을 반환한다.

(numerator 2/3)     ;=> 2
(numerator 2/4)     ;=> 1

(denominator 2/3)   ;=> 3

분수가 포함된 계산의 경우에도 정확한 연산이 보장된다.

(+ 1/3 2/3)   ;=> 1N
(* 1/10 10)   ;=> 1N
(+ 1/3 1)     ;=> 4/3

분수와 실수 사이의 연산 결과는 실수형이다.

(+ 1/3 1.0)   ;=> 1.3333333333333333

분수를 실수로 강제로 형변환하고 싶을 때에는 double이나 float 함수를 사용한다.

(double 1/3)   ;=> 0.3333333333333333
(float 1/3)    ;=> 0.33333334

rational? 함수는 유리수인지를 판별한다. 즉, 정수나 분수이면 true를 반환하고, 그렇지 않으면 false를 반환한다.

클로저에서 분수형은 내부적으로 분자와 분모가 정수형으로 구현되어 있다. 따라서 내부적인 구현이 정수형으로 이루어진 모든 자료형의 경우, rational? 함수는 true를 반환한다고 생각하면 된다.
(rational? 1/2)   ;=> true
(rational? 1)     ;=> true
(rational? 2N)    ;=> true

(rational? 1.0)   ;=> false

rationalize 함수는 주어진 숫자를 유리수(rational number), 즉 정수나 분수로 변환한다. 무리수가 인수로 주어진 경우에는 가장 근접한 분수로 변환한다.

(rationalize 1.5)   ;=> 3/2
(rationalize 2/4)   ;=> 1/2
(rationalize 4/2)   ;=> 2
(rationalize 2)     ;=> 2
(rationalize 2.0)   ;=> 2N

(rationalize Math/PI)         ;=> 3141592653589793/1000000000000000
(rationalize (Math/sqrt 2))   ;=> 14142135623730951/10000000000000000

2.4. 산술 연산 함수

숫자 자료형을 모두 다루었으니 이제 산술 연산 함수들을 소개한다. 그에 앞서, 숫자들의 종류에 관계 없이 숫자인지의 여부를 판별해주는 함수 number?가 있다.

(number? 1)      ;=> true
(number? 1.0)    ;=> true
(number? 1/3)    ;=> true

(number? 23N)    ;=> true
(number? 0.1M)   ;=> true

(number? :a)     ;=> false
(number? nil)    ;=> false
(number? "23")   ;=> false

사칙 연산 함수부터 다루어 보자. 이 함수들은 숫자의 자료형에 관계 없이 동작한다.

(+ 1 2)       ;=> 3
(+ 3.5 4.1)   ;=> 7.6
(+ 1/3 1/4)   ;=> 7/12

(- 10 5)      ;=> 5

(* 7 8)       ;=> 56

(/ 6 3)       ;=> 2
(/ 3 4)       ;=> 3/4
(/ 3 4.0)     ;=> 0.75

2개 이상의 인수도 올 수 있다.

(+ 1 2 3 4 5)   ;=> 15
(- 10 9 8 7)    ;=> -14
(* 1 2 3 4 5)   ;=> 120
(/ 20 2 5)      ;=> 2

인수가 하나도 없으면, + 함수는 덧셈의 힝등원 0을, * 함수는 곱셈의 항등원 1을 반환한다.

(+)   ;=> 0
(*)   ;=> 1

-/ 함수의 경우는, 인수가 하나도 없으면 예외가 발생한다.

(-)   ;>> ArityException Wrong number of args (0) passed to: core/-
(/)   ;>> ArityException Wrong number of args (0) passed to: core//

인수가 한 개만 있는 경우, +* 함수는 주어진 인수를 그대로 반환하지만, - 함수는 덧셈의 역원을, / 함수는 곱셈의 역원을 반환한다.

(+ 5)   ;=> 5
(* 9)   ;=> 9

(- 3)   ;=> -3
(/ 3)   ;=> 1/3

몫(quotient)과 나머지(remainder)를 구하려면, 각각 quotrem 함수를 이용한다. 이 두 함수는 실수에도 적용된다.

(quot 10 3)      ;=> 3
(rem 10 3)       ;=> 1

(quot 3.7 1.1)   ;=> 3.0
(rem 3.7 1.1)    ;=> 0.3999999999999999

incdec 함수는 1씩 증가 또는 감소 시킨다.

(inc 10)     ;=> 11
(inc 10.1)   ;=> 11.1
(inc 1/2)    ;=> 3/2

(dec 10)     ;=> 9
(dec 10.1)   ;=> 9.1
(dec 1/2)    ;=> -1/2

maxmin 함수는 각각 주어진 인수들 중 최대값과 최소값을 반환한다.

(max 1 2 3 4 5)       ;=> 5
(min 1 2 3 4 5)       ;=> 1

(max 1.1 2 3 4 5.5)   ;=> 5.5
(min 1.1 2 3 4 5.5)   ;=> 1.1

`min-key`와 `max-key`는 주어진 인수들에 함수를 적용해서 나온 최대값과 최소값을 반환한다.

(min-key #(Math/abs %) -3 1 4)          ;=> 1
(apply min-key #(Math/abs %) [-3 1 4])  ;=> 1

(max-key #(Math/abs %) -3 1 4)          ;=> 4
(apply max-key #(Math/abs %) [-3 1 4])  ;=> 4

기타 수학 함수는 java.lang.Math 클래스의 정적(static) 메소드를 이용해야 한다.

Math/PI         ;=> 3.141592653589793
Math/E          ;=> 2.718281828459045

(Math/sqrt 2)   ;=> 1.4142135623730951

(Math/sin (/ Math/PI 2))   ;=> 1.0
(Math/cos 0)               ;=> 1.0

2.5. 등가/대소 비교 함수

숫자들의 등가를 비교할 떄에는 = 함수보다는 == 함수를 사용하는 것이 바람직하다. = 함수는 숫자들이 인수로 올 경우 숫자들의 타입까지 같아야 true를 반환하기 때문이다.

(= 1 1.0)         ;=> false
(= 0.5 1/2)       ;=> false

(= 1.0 1.0 1.0)   ;=> true
(= 2 2 2)         ;=> true

반면에 == 함수는 타입이 같지 않아도 수학적으로 등가이면 true를 반환한다.

(== 1 1.0)         ;=> true
(== 0.5 1/2)       ;=> true

(== 1.0 1.0 1.0)   ;=> true
(== 2 2 2)         ;=> true

그리고 == 함수는 인자들이 반드시 숫자형이어야만 한다. 그렇지 않으면 예외가 발생한다. 따라서 == 함수는 숫자 전용 등가 비교 함수로 기억하면 된다.

(== :a :a)
;>> ClassCastException clojure.lang.Keyword cannot be cast to java.lang.Number

반면에, = 함수는 숫자 이외의 자료형을 비교하는 데 주로 사용된다.

(= :a :a)         ;=> true
(= "cat" "cat")   ;=> true
(= [2 3] [2 3])   ;=> true

대소 비교 함수에는 <, <=, >, >=가 있다.

(< 10 15)    ;=> true
(<= 10 10)   ;=> true

이 함수들은 2개 이상의 인수들이 올 수 있다. 예를 들어 (< a b c d)와 같은 식이 있으면, 이는 수학적으로 a < b < c < d를 모두 만족해야 true를 반환한다.

(< 1 2 3 4 5)     ;=> true
(< 1 2 3 3 5)     ;=> false

(<= 1 2 3 3 5)    ;=> true

2.6. 비트 연산

(bit-and 2r1100 2r1001)   ;=> 8
(bit-or  2r1100 2r1001)   ;=> 13
(bit-xor 2r1100 2r1001)   ;=> 5

2.7. 기타 유용한 함수들

zero? 함수는 0인지를 판별한다.

(zero? 0)     ;=> true
(zero? 0.0)   ;=> true

(zero? 1)     ;=> false
(zero? -1)    ;=> false

(zero? 1.0)   ;=> false
(zero? 1/2)   ;=> false

pos? 함수와 neg? 함수는, 각각 양수인지 음수인지를 판별한다.

(pos? 1)     ;=> true
(pos? 1.0)   ;=> true
(pos? 1/2)   ;=> true

(pos? 0)     ;=> false
(pos? -1)    ;=> false

(neg? -1)     ;=> true
(neg? -1.0)   ;=> true
(pos? -1/2)   ;=> true

(neg? 0)     ;=> false
(neg? 1)     ;=> false

odd? 함수와 even? 함수는, 각각 홀수인지 짝수인지를 판별한다. 인수가 정수가 아니면 예외가 발생한다.

(odd? 1)    ;=> true
(odd? 2)    ;=> false
(odd? 0)    ;=> false

(even? 2)   ;=> true
(even? 1)   ;=>false

(odd? 1.0)
;>> IllegalArgumentException Argument must be an integer: 1.0

3. nil

클로저의 nil은 자기 자신으로 평가되는 값이다. 자바의 null, 루비의 nil, 파이썬의 None과 같이 값의 부재를 표현한다.

nil          ;=> nil

(type nil)   ;=> nil

nil은 클로저에서 논리적인 거짓(logically false)으로 취급된다. 클로저에서는 falsenil만을 논리적 거짓으로 판단하고, 그 이외의 모든 값은 논리적 참(logically true)으로 판단한다. 예를 들어 0이나 빈 문자열 "", 빈 백터 [] 등도 논리적 참으로 취급된다.

(if nil 10 20)     ;=> 20
(if false 10 20)   ;=> 20

(if 0 10 20)       ;=> 10
(if "" 10 20)      ;=> 10
(if [] 10 20)      ;=> 10

nil? 함수는 주어진 인수가 nil인지 판별한다.

(nil? nil)     ;=> true

(nil? false)   ;=> false
(nil? 0)       ;=> false
(nil? "")      ;=> false
(nil? [])      ;=> false

some? 함수는 주어진 인수가 nil이 아닌지 판별한다. (not (nil? x) 와 같다.

some? 함수는 클로저 버전 1.6부터 도입되었다. core 함수치고 꽤 늦게 도입된 것인데, 그 아래 버전에서는 컴파일 예외 발생하니 사용시 버전 주의.
(some? nil)      ;=> false

(some? false)    ;=> true
(some? 0)        ;=> true
(some? "")       ;=> true
(some? [])       ;=> true

4. 불린(Booleans)

불린값으로는 truefalse가 있고, 역시 자기 자신으로 평가되는 값이다.

true    ;=> true
false   ;=> false

(type true)    ;=> java.lang.Boolean
(type false)   ;=> java.lang.Boolean

true? 함수는 주어진 인수가 '실제로 true'[4]인지 판별한다. 즉, 주어진 인자가 true로 평가될 때에만 true를 반환한다. 그리고 false? 함수는 주어진 인수가 실제로 false인지 판별한다. 즉, 주어진 인자가 false로 평가될 때에만 true를 반환한다.

(true? true)      ;=> true
(true? (= 1 1))   ;=> true

(true? "hello")   ;=> false
(true? 1)         ;=> false

(false? false)     ;=> true
(false? (= 1 2))   ;=> true

(false? nil)       ;=> false
(false? "foo")     ;=> false
(false? 1)         ;=> false

5. 키워드(Keywords)

키워드는 임의의 식별자 앞에 콜론(:) 기호를 붙여 만든다. 자기 자신으로 평가되는 값으로 클로저에서 아주 많이 사용된다.

:city              ;=> :city

(type :city)       ;=> clojure.lang.Keyword

(keyword? :city)   ;=> true

;; keyword함수는 문자열을 키워드로 바꾸어 준다.
(keyword "city")   ;=> :city
(= :city (keyword "city")) ;=> true

콜론은 키워드를 만들기 위한 문법적 기능을 수행하기 위한 것이지 키워드의 이름은 아니다.

(= :seoul (keyword "seoul"))
(= :seoul (keyword ":seoul"))

키워드는 특히 키/값 쌍으로 이루어진 맵 자료형의 키로 많이 사용된다.

(def person {:name "Sandra Cruz"
             :city "Portland, ME"})

식별자 앞에 콜론을 두 개 붙이면 해당 이름공간을 가진 키워드로 확장된다. 예를 들어, 현재의 이름공간이 user일 때 다음을 실행하면, user 이름공간이 붙은 키워드로 확장되는 것을 볼 수 있다.

::address   ;=> :user/address

이때 namespace 함수와 name 함수로, 이름공간과 키워드명 부분만을 알아낼 수 있다.

(name :user/address)        ;=> "address"
(name ::address)            ;=> "address"

(namespace ::address)       ;=> "user"
(namespace :user/address)   ;=> "user"

(namespace :address)        ;=> nil

6. 심볼(Symbols)

클로저의 심볼은 다른 언어들에서의 식별자(identifiers)와 유사한 개념이다. 심볼은 숫자가 아닌 문자로 시작해야 하고, 그 뒤에 *, +, -, =, ?, !, $, %, &, _, |, <, > 같은 기호와 문자들이 올 수 있다.

(def *+-=?!$%&_|<> "이런 심볼도 가능") ;=> "이런 심볼도 가능"

심볼은 유니코드를 지원한다.

(def 1 "한글도 가능") ;=> "한글도 가능"

심볼은 일반적으로 어떤 값을 가리킨다. 그래서 심볼이 평가되면 가리키던 값을 반환하게 된다.

;; 전역 심볼 a를 정의한다.
(def a 10)

;; 전역 심볼 a를 평가하면, 가리키던 값 10을 반환한다.
a   ;=> 10

;; 지역 심볼 a를 정의한다. let 안에서 지역 심볼 a는 전역 심볼 a을 가린다(shadowing).
(let [a 20]
  (+ 100 a))
;=> 120

;; 전역 심볼 a는 여전히 10을 가리키고 있다.
a   ;=> 10

name 함수는 심볼을 문자열로 바꾼다.

(name 'a) ;=> "a"

symbol 함수는 문자열을 심볼로 만든다.

(symbol "a") ;=> a
(symbol "my-namespace" "a") ;=> my-namespace/a

symbol? 함수는 심볼인지 여부를 확인한다.

(def a 1)      ;=> 1
(symbol? 'a)   ;=> true
(symbol? a)    ;=> false

(symbol? "a")  ;=> false
(symbol? :a)   ;=> false
(symbol? 1)    ;=> false

7. 문자(Characters)

문자는 역슬래쉬(\) 기호 뒤에 한 개의 문자를 덧붙여 표현한다.

\a    ;=> \a
\가   ;=> \가

(type \a)    ;=> java.lang.Character
(char? \a)   ;=> true

\u 뒤에 16진법 숫자 4개를 덧붙여 유니코드를 직접 표현할 수도 있다. \o[5] 뒤에 8진법 숫자를 붙여 표현할 수도 있는데, \o0 ~ \o377 (즉, 0 ~ 255) 사이의 숫자만 허용한다.

\u03bb   ;=> \λ
\o101    ;=> \A

이름을 갖는 특별한 문자들이 다음과 같이 정의되어 있다.

  • \space

  • \newline

  • \formfeed

  • \return

  • \backspace

  • \tab

`char`함수는 정수를 문자로 바꾼다.

(char 97)     ;=> \a

`char?`는 문자인지를 판별한다.

(char? \a)    ;=> true
(char? 97)    ;=> false
(char? "a")   ;=> false

8. 문자열(Strings)

클로저에서 문자열 역시 자기 자신으로 평가되는 값(value)으로, 쌍따옴표(")로 둘러싸 표현한다.

"Welcome to Clojure!"          ;=> "Welcome to Clojure!"

(type "functional language")   ;=> java.lang.String
(string? "hello")              ;=> true

자바의 java.lang.String 클래스를 그대로 이용하므로, String 클래스의 메소드도 이용할 수 있다.

(.toUpperCase "modern lisp")   ;=> "MODERN LISP"

여러 줄의 입력도 가능하다.

"multiline strings
are allowed too."
;=> "multiline strings\nare allowed too."

str 함수는 주어진 인수들을 문자열로 변환한 후, 공백 없이 연결된 문자열을 반환한다. 단, nil은 빈 문자열("")로 변환된다.

(str)      ;=> ""
(str nil)  ;=> ""

(str 1)          ;=> "1"
(str 1 2 nil 3)  ;=> "123"

(str [1 2 3])    ;=> "[1 2 3]"

(str 1 'symbol :keyword)   ;=> "1symbol:keyword"
(str "Hello, " "World!")   ;=> "Hello, World!"

format 함수는 java.lang.String.format 함수를 그대로 이용한다. 자세한 포맷 서식은 java.util.Formatter 관련 문서를 참조하도록 한다.

(format "Hello there, %s" "Bob")
;=> "Hello there, Bob"

(format "%5d" 3)
;=> "    3"

(format "Pad with leading zeros %07d" 5432)
;=> "Pad with leading zeros 0005432"

(format "Left justified :%-7d:" 5432)
;=> "Left justified :5432   :"

subs 함수는 주어진 문자열의 '시작 index(포함)'에서 '종료 index(불포함)'까지의 부분 문자열(substring)을 반환한다. index는 0부터 시작하고, '종료 index’가 주어지지 않으면, 문자열의 마지막까지 반환한다.

(subs "Clojure" 1 3)   ;=> "lo"
(subs "Clojure" 1)     ;=> "lojure"

8.1. println-str/print-str/prn-str/pr-str 함수

이 함수들은 println/print/prn/pr 함수들과 관계가 있다. 함수명에 `-str`이 붙지 않은 이 함수들은, 문자열을 '반환’하는 함수가 아니라, 문자열을 stdout(표준 출력, 일반적으로 디스플레이 화면)에 '출력’하는 함수이다.

(println "Hello world.")
;>> Hello world.
;=> nil

(println-str "Hello world.")
;=> "Hello world.\n"

위 두 함수의 출력 결과를 표시할 때, 표기 방식이 약간 다르다는 점에 먼저 주목할 필요가 있다. ;>> 기호는 그 뒤의 문자열이 '화면’에 출력되었다는 것을 나타내고, ;=> 기호는 이 함수의 반환값이 nil이라는 것을 표시하고 있다. 즉, println 함수는 side effect(부수 효과)를 수행하는 함수이다. 그리고 클로저에서 부수 효과를 실행하는 함수들은 대개 nil 값을 반환한다.

반면에 println-str 함수는, println을 사용했다면 화면에 출력되었어야 할 문자열을, 화면에 출력하지 않고 함수의 반환값으로 리턴한다. -str이 붙고 붙지 않은 나머지 함수들의 관계도 마찬가지이다.

println 함수는 개행 문자(newline)를 맨마지막에 추가하는 반면, print 함수는 개행 문자를 추가하지 않는다.

(println "foo") (println "foo")
;>> foo
;>> foo
;=> nil

(print "foo") (print "foo")
;>> foofoo
;=> nil

prn 함수와 pr 함수의 관계도 println 함수와 print 함수의 관계와 같다. 즉, 개행문자를 추가하는지 여부의 차이이다.

(prn "foo") (prn "foo")
;>> "foo"
;>> "foo"
;=> nil

(pr "foo") (pr "foo")
;>> "foo""foo"
;=> nil

이번에는 printlnprn의 차이를 알아보자. 이 두 함수는 '문자열’과 '문자’를 출력할 때에만 차이가 있다.

(println 10 "foo\nbar" \A :keyword [1 2 3])
;>> 10 foo
;>> bar A :keyword [1 2 3]
;=> nil

(prn 10 "foo\nbar" \A :keyword [1 2 3])
;>> 10 "foo\nbar" \A :keyword [1 2 3]
;=> nil

즉, 문자열 자료형을 출력할 때, println 함수는 '사람이 읽기 쉬운' 형태로 출력한다. 그래서 출력할 때, 겹따옴표(")를 제거하고 개행 문자 \n도 실제 개행을 한 결과를 보여준다. 그리고 문자 자료형인 경우에는 역슬래시(\) 기호를 제거한 상태로 출력해 준다. 이 두 자료형을 제외한 나머지 자료형들의 경우에는, 두 함수 모두 출력한 결과에 차이가 없다.

반면에 prn 함수는 클로저의 리더(reader) 함수인 readread-string 함수가 다시 읽어 들일 수 있는 형태로 출력해 준다.

(prn "hello")
;>> "hello"
;=> nil

(println "hello")
;>> hello
;=> nil

(prn-str "hello")
;>> "\"hello\"\n"

(println-str "hello")
;>> "hello\n"

다음에서 "hello" 문자열이 인수로 주어졌지만, 첫 번째 줄은 문자열 "hello"를 반환한 반면에, 두 번째 줄은 심볼 hello를 반환했다. 그래서 출력한 문자열을 문자열 그대로 클로저 리더 함수로 다시 읽어 들이고 싶다면 prn-str 또는 pr-str 함수를 사용해야 한다.

(read-string (prn-str "hello"))       ;=> "hello"
(read-string (println-str "hello"))   ;=> hello

8.2. clojure.string 이름 공간의 문자열 처리 함수들

clojure.string 이름 공간에는 문자열 처리시 유용한 함수들이 많다. 그 중 몇 개만 소개한다.

upper-caselower-case 함수는 각각 주어진 문자열을 대문자와 소문자로 바꾼다.

(require '[clojure.string :as str])

(str/upper-case "Clojure User Groups")
;=> "CLOJURE USER GROUPS"

(str/lower-case "Clojure User Groups")
;=> "clojure user groups"

split 함수는 문자열과 정규식 패턴을 받아, 분할된 문자열의 벡터를 반환한다. 클로저에서 정규식은 문자열 앞에 # 기호를 붙여 표시하는데, 자바의 정규식 표현을 따른다.

(str/split "Clojure is awesome!" #" ")
;=> ["Clojure" "is" "awesome!"]

(str/split "q1w2e3r4t5y6u7i8o9p0" #"\d+")
;=> ["q" "w" "e" "r" "t" "y" "u" "i" "o" "p"]

정규식 인수 뒤에 숫자를 지정하면, 해당하는 숫자만큼의 문자열들을 반환한다.

(str/split "q1w2e3r4t5y6u7i8o9p0" #"\d+" 5)
;=> ["q" "w" "e" "r" "t5y6u7i8o9p0"]

join 함수는 인수로 주어진 컬렉션을, 문자열로 변환환 후 연결해 반환한다.

(str/join [1 2 3])
;=> "123"

연결할 떄 사용할 문자열을 컬렉션 앞에 지정해 주는 것이 일반적이다.

(str/join ", " ["spam" "eggs" "spam"])
;=> "spam, eggs, spam"

(str/join ", " ["spam" "" "eggs" nil "spam"])
;=> "spam, , eggs, , spam"

(str/join "\n" (str/split "The Quick Brown Fox" #"\s"))
;=> "The\nQuick\nBrown\nFox"

trim 함수는 주어진 문자열의 좌우 끝에 있는 공백 문자들을 모두 제거한 문자열을 반환한다. trimltrimr 함수는 각각 주어진 문자열의 좌측과 우측 끝에 있는 공백 문자들을 제거한 문자열을 반환한다.

이 세 함수 모두 문자열 중간에 있는 공백 문자들은 제거하지 않는다.

(str/trim "        my  string         ")
;=> "my  string"

(str/triml "        my  string         ")
;=> "my  string         "

(str/trimr "        my  string         ")
;=> "        my  string"

1. radixr을 의미한다.
2. iNtegerN을 의미한다.
3. 원래는 deciMalM에서 비롯된 것이나, MoneyM으로 이해하는 사람도 있다. 이 자료형은 정확한 돈 계산이 필요할 때 주로 사용되기 때문이다.
4. 논리적 참(logically true)과는 다르다는 점에 주의하자.
5. 숫자 0이 아니라 알파벳 o임에 주의하자.