10개의 데이타 구조에 동작하는 10개의 함수보다는 한 개의 데이타 구조에 동작하는 100개의 함수가 더 좋다.
— 앨런 펄리스(Alan J. Perlis)
프로그래밍 경구

지난 장에서는 단순 자료형을 설명하였다. 이번 장에서는 복합 자료형 혹은 컬렉션을 설명한다.

위 경구는 클로저에서 컬렉션, 특히 자료구조(Data Structures)의 설계적 측면을 가장 잘 표현한 것이다. 실제로 클로저는 단일한 추상적 자료구조와 이것을 다루는 100여 개의 코어 함수를 제공하는데, 이것은 클로저의 뼈대를 이루는 핵심적 토대로서, 그 밖에 다른 모든 언어적 기능들이 기본적으로 이러한 토대에 기반한다.

1. 컬렉션

컬렉션은 여러 데이터를 하나의 덩어리로 묶어서 다루기 위한 것이다. 클로저는 컬렉션으로 사용할 수 있는 여러가지 자료구조(Data Structures)를 제공하는데 다음과 같은 특징을 갖는다.

클로저 컬렉션의 특징
  • 불변값이다(Immutable)

  • 존속적이다(Persistent)

  • 구조의 공유(Structural Sharing)

  • 동등 비교는 값으로만 한다.

  • hash 값을 제공한다.

  • 스레드 안전하다(thread-safe)

  • 추상(Abstraction)으로 표현된다.

  • java.lang.Iterable을 구현한다.

  • java.util.Collection의 일부 read-only 부분을 구현한다.

클로저 컬렉션의 이러한 특성으로 인해 클로저는 다른 리스프(LISP)언어와는 구별된다.[1] 또한 클로저 컬렉션은 클로저가 함수형 프로그래밍 언어로서의 효율적이고 효과적으로 동작하는데 았어 매우 중요한 역할을 한다. 자료구조의 이러한 특성을 잘 아는 것은 그것을 잘 사용하는 것 만큼이나 중요하다. 앞으로 본 장을 통해서 위 특성들에 대해 자세히 살펴볼 것이다.

컬렉션으로서 데이터를 어떤 방식으로 묶느냐에 따라 클로저는 다음 4개의 컬레션을 제공한다.

클로저 컬렉션
리스트

가장 단순한 컬렉션으로 순차 접근이 가능하다. 단일 연결 리스트로 되어 있다.

벡터

임의 접근이 가능하다. 다른 언어에서의 배열 정도에 해당한다.

키-값 쌍으로 구성된다. 파이썬의 dictionary, 루비의 Hash에 해당한다.

집합

중복되지 않는 요소를 갖는다.

클로저에서 제공하는 컬렉션은 리스트, 벡터, 맵, 집합이다. 그밖에 자바에서 제공하는 컬렉션도 있다. 또한 문자열도 컬렉션이다. 이 책에서는 특별한 언급이 없으면 컬렉션은 클로저의 컬렉션을 의미하한다.

1.1. 컬렉션 리터럴(Literals)

컬렉션 리터럴은 컬렉션을 어떻게 표기하는지를 나타내는 것이다. 다음은 리스트, 벡터, 맵, 집합의 리터럴 표기를 보여준다.

;;; 컬렉션은 여러가지 자료형의 단순값을 요소로 취할 수 있다.

'(1 1.0 :a \a "a")  ; 리스트 리터럴 (1)
;=> (1 1.0 :a \a "a")

[1 1.0 :a \a "a"]   ; 벡터 리터럴   (2)
;=> [1 1.0 :a \a "a"]

{:a 1 :b 2 :c 3}    ; 맵 리터럴     (3)
;=> {:a 1, :c 3, :b 2}  (4) (5)

#{1 1.0 :a \a "a"}  ; 집합 리터럴   (6)
;=> #{1.0 1 :a \a "a"} (7)
1 리스트 : 괄호 ()로 묶고, 앞에 인용 기호를 붙인다.
2 벡터 : 대괄호 []로 묶는다.
3 맵 : 중괄호 {)로 묶는다.
4 리스트와 벡터와는 달리 맵은 요소 간에 순서가 없다. 그래서 정의했을 때의 순서가 보장되지 않는다.
5 쉼표(,)는 스페이스로 취급되기 때문에, 있으나 없으나 상관없다.
6 집합 : #이 앞에 붙은 중괄호 #{}로 묶는다.
7 집합도 맵과 마찬가지로 요소 간에 순서가 없다.
리스트는 특별하다

클로저에서 리스트는 특별하다. 리스트는 데이타를 묶는 컬렉션으로서의 역할도 있지만, 다른 리스프(LISP)언어에서처럼 함수를 호출하는 역할도 있다. 리스트가 함수를 호출하는 역할은 하지 않고, 다만 데이타를 묶는 역할만 하도록 하기 위해 인용 기호(')를 괄호 앞에 붙인다.

;; 첫 요소인 +를 함수로서 호출한다.
(+ 1 2)
;=> 3

;; 첫 요소인 1을 함수로서 호출한다. 하지만 1은 함수가 아니라서 예외가 발생한다.
(1 2 3)
;>> ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn

;; 인용부호(')를 붙이면 리스트는 첫 요소를 함수로 호출하는 역할을 하지 않는다.
;; 단지 리스트 자료형의 역할만 한다.
'(1 2 3)
;=> (1 2 3)

;; +를 함수로서 호출하지 않는다. +는 리스트의 첫 요소일 뿐이다.
'(+ 1 2)
;=> (+ 1 2)

하지만 데이타가 없는 빈 리스트는 함수로서 취급할 요소가 없으므로 인용 기호(')가 없어도 예외가 발생하지 않는다.

(= () '())
;=> true

컬렉션의 요소로 컬렉션이 올 수 있다.

'((1) [1] {:a 1} #{1})  ; 리스트 안에 리스트, 벡터, 맵, 집합이 있다. (1)
;=> ((1) [1] #{1} {:a 1})

['(1) [1] {:a 1} #{1}]  ; 벡터 안에 리스트, 벡터, 집합, 맵이 있다.
;=> [(1) [1] #{1} {:a 1}]

{'(1) [1] #{1} {:a 1}}  ; 맵 안에 리스트, 벡터, 맵, 집합이 있다.
;=> {(1) [1] {:a 1} #{1}} (2)

#{'(1) [1] {:a 1} #{1}} ; 집합 안에 리스트, 벡터, 맵, 집합이 있다.
;=> #{[1] #{1} {:a 1}}  (3)
1 내포된 리스트에는 인용 부호(')를 붙이지 않아도 된다.
2 맵의 키로 컬렉션이 사용될 수 있다.
3 (= '(1) [1])이기 때문에 [1]만 남는다.

맵의 경우에는 같은 키가 중복되는 것을 허용하지 않는다. 같은 키가 있으면 예외가 발생한다.

;; 키 :a가 중복되어 예외가 발생한다.
{:a 1 :a 2}
;>> IllegalArgumentException Duplicate key: :a

집합은 같은 값이 중복되는 것을 허용하지 않는다. 같은 값이 있으면 예외가 발생한다.

;; 3이 중복되어 예외가 발생한다.
#{1 2 3 3}
;>> IllegalArgumentException Duplicate key: 3
위의 두 코드를 보면 집합이나 맵이나 중복이 있으면 Duplicate key라는 예외가 발생한다는 것을 알 수 있다. 이 예외는 key가 중복되어서 발생한 것인데, 맵은 하나 키가 여러 값을 가질 수 없으니 이해가 된다. 하지만 집합은 키가 아니라 값이 중복된 것인데, 왜 Duplicate key 예외가 발생할까? 이것은 집합의 내부 구현은 맵과 같다는 것을 의미한다. 즉 집합은 키와 값이 같은 맵으로 볼 수 있다는 것이다. 예를 들어, #{1 2 3}{1 1 2 2 3 3}과 같다고 볼 수 있는 것이다.

맵은 키-값 쌍이 맞지 않으면 예외가 발생한다. 맵 안에는 항상 짝수 개의 형식(form)이 있어야 한다.

;; 키 :b에 해당하는 값이 없어서 예외가 발생한다.
{:a 1 :b}
;>> RuntimeException Map literal must contain an even number of forms

컬렉션은 구조화된 데이터의 묶음이지만, 또한 그 자체로 값(value)이다. 따라서 데이타가 없는 컬렉션, 즉 빈(empty) 컬렉션도 값(value)이다. 아무것도 없음을 의미하는 nil과는 다르다.

(=  () nil) ;=> false ; empty list
(=  [] nil) ;=> false ; empty vector
(= #{} nil) ;=> false ; empty set
(=  {} nil) ;=> false ; empty map

1.2. 컬렉션의 생성

리스트, 벡터, 집합, 맵 등 각 컬렉션을 동적으로 생성하는 함수들이 있다.

(list 1 2 3)            ; 리스트 생성
;=> (1 2 3)

(vector 1 2 3)          ; 벡터 생성
;=> [1 2 3]

(hash-map :a 1 :b 2)    ; 맵 생성    (1)
;=> {:a 1 :b 2}

(hash-set 1 2 3)        ; 집합 생성   (2)
;=> #{1 2 3}
1 map은 전혀 다른 함수이다.
2 set은 다른 컬렉션을 집합으로 바꾸는 함수이다.

hash-maphash-set은 중복이 있으면 제거한다.

(hash-set 1 2 3 3 2)       ; 2와 3이 중복
;=> #{1 2 3}

(hash-map :a 1 :b 2 :a 10) ; :a키가 중복  (1)
;=> {:a 10 :b 2}
1 키가 중복이 되면 나중의 것이 채택된다. 기존값을 덮어쓴다고 생각하면 된다.

vec은 다른 컬렉션을 받아 벡터를 만든다.

(vec '(1 2 3))
;=> [1 2 3]

(vec #{1 2 3})
;=> [1 3 2]

(vec {:a 1 :b 2 :c 3})
;=> [[:c 3] [:b 2] [:a 1]]

(vec "abcd")
;=> [\a \b \c \d]

subvec은 벡터에서 일부를 시작(닫힘)과 끝(열림)을 인덱스로 지정해서 뽑아낼 수 있다. 끝을 지정하지 않으면 벡터의 갯수가 기본으로 지정된다.

(subvec [1 2 3 4 5 6 7] 2)
;=> [3 4 5 6 7]

(subvec [1 2 3 4 5 6 7] 2 4)
;=> [3 4]

set은 다른 컬렉션을 받아 집합을 만든다.

(set '(1 1 2 3 2 4 5 5))      ; 중복은 제거된다
;=> #{1 2 3 4 5}

(set [1 1 2 3 2 4 5 5])       ; 중복은 제거된다
;=> #{1 2 3 4 5}

(set [1 2 3 4 5])
;=> #{1 2 3 4 5}

(set {:a 1 :b 2 :c 3})
;=> #{[:b 2] [:c 3] [:a 1]}

(set "abcd")
;=> #{\a \b \c \d}

1.3. seq

클로저에서 모든 컬렉션은 시퀀스로 취급될 수 있다. 시퀀스는 head와 tail 두 개로 구성되는데, tail은 또 다른 시퀀스이다. 클로저에서의 시퀀스에 대해서는 뒤에서 보다 더 자세히 살펴볼 것이다.

seq 함수는 컬렉션을 시퀀스로 변환해 반환한다.

(seq '(1))    ;=> (1)
(seq [1])     ;=> (1)
(seq #{1})    ;=> (1)
(seq {:a 1}   ;=> ([:a 1])

1.4. 컬렉션에 요소 추가/삭제

클로저에서는 컬렉션에 요소를 추가하거나 삭제하면, 기존 컬렉션에 요소가 추가/삭제된 새로운 컬렉션이 만들어진다. 기존 컬렉션은 변하지 않고 그대로 있다. 컬렉션은 불변이다(immutable). 이것은 기존 컬렉션을 복사한 후 요소를 추가/삭제하는 것처럼 비효율적인 방식은 아니다. 구조 공유(structural sharing)이라는 기법을 통해 아주 효율적으로 불변성을 지원한다.

이러한 컬렉션의 불변성은 멀티스레딩에 근본적으로 안전한 프로그래밍을 가능하게 한다.

1.4.1. cons

cons[2] 함수는 두 개의 인수를 받아 새로운 seq를 반환한다. 반환된 seq의 head는 첫 번째 인수이고, 컬렉션인 두 번째 인수의 seq가 tail이 된다.

(cons 0 '(1 2 3))  ; 리스트
;=> (0 1 2 3)

(cons 0 [1 2 3])   ; 벡터
;=> (0 1 2 3)

(cons 0 {:a 1 :b 2})    ; 맵
;=> (0 [:a 1] [:b 2])   (1)

(cons 0 #{1 2 3})  ; 집합
;=> (0 1 3 2)
1 (seq {:a 1 :b 2}) ;⇒ ([:a 1] [:b 2]) 이다. 즉 맵의 seq 표현은 키-값 쌍의 튜플들이다.

1.4.2. conj

conj[3]함수는 컬렉션인 첫번째 인수에 두번째 인수를 추가한 새로운 컬렉션을 반환한다. conj는 입력 컬렉션의 형태가 보존되며, 요소 추가는 해당 컬렉션에 가장 효율적인 방식으로 처리된다.

(conj '(1 2 3) 0)    ; 리스트는 맨 앞에 추가된다.
;=> (0 1 2 3)

(conj [1 2 3] 0)     ; 벡터는 맨 뒤에 추가된다.
;=> [1 2 3 0]

(conj {:a 1} [:b 2]) ; 맵은 키-값 쌍의 벡터 튜플로 추가된다.
;=> {:a 1 :b 2}

(conj #{1 2 3} 0)    ; 집합의 경우 추가되어도 순서는 없다.
;=> #{0 1 2 3}

리스트는 순차 접근이기 때문에 맨 마지막이 아니라 맨 앞에 추가되는 것이 효율적이다. 반면 벡터는 임의 접근이기 때문에 맨 마지막에 추가되는 것이 효과적이다.

consconj는 리스트에 요소를 추가할 때는 똑같이 맨 앞에 추가한다. 하지만 벡터에 요소를 추가할 때는 서로 다르게 cons는 맨 앞에, conj는 맨 뒤에 추가한다.

맵의 경우에는 요소를 추가하기 위해서는 키-값 쌍의 벡터로 된 튜풀을 제공해야 한다.

conj는 여러 개의 요소를 한 번에 추가할 수 있다.

(conj '(4 5 6) 1 2 3)              ; 리스트.
;=> (3 2 1 4 5 6)

(conj [4 5 6] 1 2 3)               ; 벡터.
;=> [4 5 6 1 2 3)

(conj {:a 1} [:b 2] [:c 3] [:d 4]) ; 맵.
;=> {:d 4 :c 3 :b 2 :a 1}

(conj #{4 5 6} 1 2 3)              ; 집합.
;=> #{1 2 3 4 5 6}

1.4.3. disj

집합의 경우 disj[4] 함수로 요소를 제거한 새로운 집합을 만들 수 있다.

(disj #{1 2 3} 2)    ; 2를 제거.
;=> {1 3}

(disj #{1 2 3} 1 3)  ; 1과 3을 제거.
;=> {2}

(disj #{1 2 3} 4)    ; 제거할 요소가 없다.
;=> {1 2 3}

1.4.4. assoc

맵의 경우 요소를 추가한 새로운 맵을 만들 때 주로 assoc[5]을 쓴다.

(assoc {} :a 1 :b 2 :c 3)       ; 빈맵에 여러 요소 추가
;=> {:c 3, :b 2, :a 1}

(assoc nil :a 1)           ; nil은 빈맵으로 취급된다. (1)
;=> {:a 1}

(assoc {:a 1 :b 2} :a 10 :c 3)  ; 같은 키가 이미 있으면 그 값을 덮어쓴다.
;=> {:c 3, :a 10, :b 2}
1 클로저에서 nil이 빈맵으로 취급되는 이유는 시퀀스와 관련된 역사적 이유가 있다. 이것에 대해서는 나중에 시퀀스와 관련해서 설명하기로 한다.

assoc은 벡터에도 사용될 수 있다. 이 때 키는 인덱스로 지정할 수 있다. 인덱스는 0부터 시작한다.

(assoc [1 2 3] 0 10)      ; 인덱스 0에 있는 1을 10으로 바꿈.
;=> [10 2 3]

(assoc [1 2 3] 2 '(4 6))  ; 인덱스 2에 있는 마지막 값인 3을 '(4 6)으로 바꿈.
;=> [1 2 (4 6)]

(assoc [1 2 3] 3 10)      ; 인덱스 3, 벡터의 끝에 10 추가. (1)
;=> [1 2 3 10]

(assoc [1 2 3] 4 10)      ; 지정한 인덱스가 없다 >> 예외 발생 (2)
;>> java.lang.IndexOutOfBoundsException
1 맨 마지막은 끝을 의미하는 nil이 있다.
2 맵의 경우 자동으로 추가되었다.

위에서 보는 것처럼 벡터가 assoc에 대해 동작하는 것을 보면, 벡터는 인덱스를 키로 하는 맵으로 취급되는 것을 알 수 있다.

1.4.5. dissoc

맵에서 특정 키를 제거한 새로운 맵을 만들 때 주로 dissoc을 쓴다.

(dissoc {:a 1 :b 2 c: 3} :b)    ; :b 키 하나 제거.
;=> {:a 1, :c 3}


(dissoc {:a 1 :b 2 c: 3} :c :b) :    .
;=> {:a 1}

반면 assoc과는 다르게 dissoc은 벡터에는 사용할 수 없다.

(dissoc [0 1 2] 0)  ; 잘못된 용법 >> 예외 발생
;>> java.lang.ClassCastException

1.4.6. assoc-in

클로저에서는 벡터와 맵을 특히 많이 쓴다. 그런데 벡터 안에 맵이 있거나 또는 맵안에 벡터가 있는 경우 추가/삭제를 하는 것이 매우 번거로워지는데, 이럴 때 손쉽게 사용할 수 있는 것이 assoc-in 함수이다. 이때 내포된 컬렉션의 키를 지정하기 위해 키들의 시퀀스를 사용한다.

(def m {:a {:c 1} :b {:d 2}})

(assoc-in m [:a :c] 10)     ; (1)
;=> {:a {:c 10} :b {:d 2}}

(assoc-in m [:b :e] 3)      ; (2)
;=> {:a {:c 1}, :b {:e 3, :d 2}}
1 두 번째 인자로 지정된 값을 세 번째 인자로 덮어 쓴다. 두번째 인자 [:a :c]는 대상을 지정하기 위한 키들이다. 즉 :am에서 {:c 1}를 지정하고, :c는 바로 앞에서 지정된 맵인 {:c 1}에서 1을 지정한다. 이렇게 지정된 값을 10으로 덮어쓴다.
2 해당 키가 없으면 추가한다.

assoc이 벡터에 대해서도 동작한 것처럼 assoc-in도 벡터에 대해 동작한다.

(assoc-in {:a [1 2 3]} [:a 0] 10)    ; 맵안의 벡터
;=> {:a [10 2 3]}

(assoc-in [{:a 1}] [0 :a] 10)        ; 벡터안의 맵
;=> [{:a 10}]

(assoc-in [[1] [2] [3]] [2 0] 30)    ; 벡터안의 벡터
;=> [[1] [2] [30]]

(assoc-in [[1 1 1]                   ; 2차원 배열
           [1 1 1]
           [1 1 1]] [0 0] 0)
;=> [[0 1 1][1 1 1][1 1 1]]

1.5. 컬렉션에서 요소 참조

1.5.1. nth

nth는 컬렉션에서 지정한 인덱스에 있는 값을 반환한다. 만약 해당 인덱스가 없으면 예외가 발생한다. 인덱스가 없을 때 반환할 수 있는 디폴트값을 세번째 인자로 줄 수 있다.

(nth [1 2 3] 0)      ; 인덱스 0
;=> 1

(nth [1 2 3] 3)      ; 인덱스 3은 없다 >> 예외 발생
;>> java.lang.IndexOutOfBoundsException

(nth [1 2 3] 3 "not-found")   ; 세번째 인자는 디폴트값.
;>> "not-found"

nth는 순서가 있는 컬렉션들에 대해 동작한다.

(nth '(1 2 3) 2)               ; 리스트  (1)
;=> 3

(nth "abcd" 2)                 ; 문자열
;=> \c

(nth (into-array [1 2 3]) 2)   ; 자바 Long 배열
;=> 3

(type (into-array [1 2 3]))
;=> [Ljava.lang.Long;
1 리스트의 경우 인덱스가 없어서, 처음부터 하나씩 세면서 찾는다.

하지만 순서가 없는 맵과 집합에는 동작하지 않는다.

(nth {:a 1} 0)   ; 맵에는 인덱스를 지정할 수 없다 >> 예외 발생
;>> java.lang.UnsupportedOperationException

(nth #{1 2 3} 0) ; 집합에는 인덱스를 지정할 수 없다 >> 예외 발생
;>> java.lang.UnsupportedOperationException

반면 컬렉션에서 특정 요소의 인덱스를 구하려면 indexOf 자바 메소드를 호출하면 된다.

(.indexOf '(1 2 3) 1)   ;=> 0
(.indexOf  [1 2 3] 2)   ;=> 1
(.indexOf "abcd" "c")   ;=> 2

1.5.2. get

get은 맵에서 특정 키에 해당하는 값을 반환한다. 만약 해당키가 없으면 nil을 반환하는데, 디폴트값이 주어졌으면 디폴트값을 반환한다.

(get {:a 1 :b 2} :b)    ; :b 키의 값을 참조
;;=> 2

(get {:a 1 :b 2} :c)    ; :c 키는 없다
;;=> nil

(get {:a 1 :b 2} :c "not-found")  ; 세 번째 인자는 디폴트값
;;=> "not-found"

벡터는 인덱스를 키로 하는 맵으로 취급될 수 있기 때문에, get이 동작한다.

(get [1 2 3] 1)         ; 인덱스 1을 키 1로 사용.
;=> 2

(get [1 2 3] 5)         ; 인덱스 5를 키 5로 사용. 하지만 없다
;=> nil

인덱스가 없는 경우, nth는 예외를 던지고, get은 nil을 반환한다.

(nth [1 2 3] 5)   ;>> java.lang.IndexOutOfBoundsException
(get [1 2 3] 5)   ;=> nil

하지만 인덱스가 없는 리스트에는 get이 동작하지 않는다.

(get '(1 2 3) 0)
;=> nil

(get '(1 2 3) 1)
;=> nil

자바의 맵에도 get은 동작한다.

(get (System/getenv) "SHELL")
;;=> "/bin/bash"

(get (System/getenv) "PATH")
;;=> "/usr/local/bin:/sbin:/usr/sbin:/usr/bin:/bin"

1.5.3. get-in

맵안의 맵처럼 내포된 맵을 참조하기 위해서는 get-in을 사용한다. 만약 해당값이 없으면 nil을, 디폴트값이 주어지면 디폴트 값을 반환한다.

(def m {:language "clojure"
        :authour {:name "Rich Hickey"
                  :address {:city "Austin" :state "TX"}}})

(get-in m [:authour :name])
;=> "Rich Hickey"

(get-in m [:authour :address :city])
;=> "Austin"

(get-in m [:authour :address :zip-code])
;=> nil

(get-in m [:authour :address :zip-code] "no zip code!")
;=> "no zip code!"

get-in은 벡터에도 동작한다.

(def v [[:000-00-0000 "TYPE 1" "JACKSON" "FRED"]
        [:000-00-0001 "TYPE 2" "SIMPSON" "HOMER"]
        [:000-00-0002 "TYPE 4" "SMITH" "SUSAN"]])

(get-in v [0 2])
;=> "JACKSON"

(get-in [[0 1 1]
         [1 1 1]
         [1 1 1]] [0 0])
;=> 0

1.5.4. find

find는 맵의 엔트리(entry)를 반환한다.

(find {:a 1 :b 2 :c 3} :a)
;=> [:a 1]

(find [:a :b :c :d] 2)
;=> [2 :c]

1.5.5. contains?

get 함수는 해당키가 없는 경우에 nil을 반환한다. 하지만 조사하려는 컬렉션에 nil이 있는 경우 문제가 된다.

(get [1 2 3] 0)         ;=> 1
(get [nil 2 3] 0)       ;=> nil  (1)

(get {:a 1   :b 2} :a)  ;=> 1
(get {:a nil :b 2} :a)  ;=> nil  (2)
1 반환값이 nil이므로 값이 없음을 의미하지만, 실제로는 인덱스 0nil이라는 값이 있는 것이다.
2 반환값이 nil이므로 값이 없음을 의미하지만, 실제로는 :a 키에 nil 값이 있는 것이다.

이런 경우에 contains? 함수를 사용하면 된다.

(contains? [1 2 3] 0)         ;=> true
(contains? [nil 2 3] 0)       ;=> true

(contains? {:a 1 :b 2} :a)    ;=> true
(contains? {:a nil :b 2} :a)  ;=> true

1.5.6. keys와 vals

맵의 경우, 키들만 뽑아내거나 혹은 값들만 뽑아내는 함수로 keysvals가 있다.

(keys {:a 1 :b 2 :c 3})
;=> (:a :b :c)

(vals {:a 1 :b 2 :c 3})
;=> (1 2 3)

1.5.7. key와 val

사실 이것은 각 맵 엔트리(Entry)에 keyval를 적용한 것과 같다.

(map key {:a 1 :b 2 :c 3})
;=> (:a :b :c)

(map val {:a 1 :b 2 :c 3})
;=> (1 2 3)

keyval은 맵의 엔트리에만 적용된다. 맵의 엔트리는 clojure.lang.IPersistentVector를 구현하기 때문에 벡터처럼 표현되기는 하지만, keyval이 벡터에 적용되지는 않는다.

(first {:a 1 :b 2})
;=> [:a 1]

(key (first {:a 1 :b 2}))
;=> :a

(val (first {:a 1 :b 2}))
;=> 1

(key [:a 1])
;>> java.lang.ClassCastException: clojure.lang.PersistentVector cannot be cast to java.util.Map$Entry

(val [:a 1])
;>> java.lang.ClassCastException: clojure.lang.PersistentVector cannot be cast to java.util.Map$Entry

(type (first {:a 1 :b 2}))
;=> clojure.lang.MapEntry

(vector? (first {:a 1 :b 2}))
;=> true

1.5.8. 직접 참조

get 함수를 통하지 않고 직접 벡터, 집합, 맵으로부터 값을 읽을 수 있다. 이것이 가능한 이유는 클로저에서는 벡터, 맵, 집합이 함수처럼 동작할 수 있기 때문이다. (리스트 제외)

([1 2 3] 1)          ; 벡터일 경우, 인자 1은 인덱스로 동작
;=> 2

({:a 1 :b 2} :a)     ; :a 키의 값을 읽는다
;=> 1

({:a 1 :b 2} :c)     ; :c 키는 없다
;=> nil

({:a 1 :b 2} :c "not-found")   ; 두 번째 인자는 디폴트값 (1)
;=> "not-found"

(#{1 2 3} 1)         ; 집합일 경우, 인자 1은 키로서 동작 (2)
;=> 1
1 맵의 경우, 디폴트값을 줄 수 있다.
2 집합의 경우 이러한 용법은 그 집합에서의 특정값의 존재 여부를 확인하는 방법으로 쓰인다. 실제로 상당히 요긴하게 쓰인다.

이러한 컬렉션 외에 키워드나 심볼 등도 함수처럼 동작한다.

(:a {:a 1 :b 2})
;=> 1

('a {'a 1 'b 2})
;=> 1

보통 맵의 경우 위와 같은 방식으로 쓰는 것이 더 안전하다. 왜냐하면 함수가 인자로 맵을 받을 때, 때로 그 인자로 맵이 아닌 nil이 전달되는 경우가 심심치 않게 있는데, 그 인자를 함수로 호출하게 되면, nil을 함수로 호출하는 것이 되어 예외가 발생하기 때문이다.

(def m {:a 1 :b 2})

(m :a)
;=> 1

(:a m)
;=> 1

(def m nil)

(m :a)   ; nil은 함수가 아니다 >> 예외 발생
;>> java.lang.NullPointerException

(:a m)   ; nil이 인자로 사용된다  (1)
;=> nil
1 m이 맵이 아니라 nil이면, 아무값도 없다는 의미로 nil을 반환하는 것은 정상적이다.
어떤 것이 함수로 호출 가능한지 알아보는 방법은 ifn? 진위함수를 사용하는 것이다. 이 함수는 그 대상이 IFn 인터페이스를 구현했는지 여부를 확인한다. 클로저는 IFn 인터페이스를 구현한 것을 함수로서 호출한다. 실제로 키워드를 테스트해 보면, (ifn? :a) ;=> true, 그리고 심볼을 테스트해 보면, (ifn? 'a) ;=> true 이 된다. 물론 (ifn? 3) ;=> false이다. 반면 진짜 함수의 구현 여부를 확인하려면 fn? 진위함수를 사용한다. 이 함수는 Fn 인터페이스를 구현했는지 여부를 확인한다.

1.5.9. 고차함수에서 직접 참조 사용하기

컬렉션, 키워드, 심볼 등이 함수처럼 동작하기 때문에, 이들을 고차함수(HOF, Higher Order Function)의 인자로 사용하면 코드가 매우 간결해져서, 클로저 프로그래밍에서 일상적으로 많이 사용된다.

(map {1 "one" 2 "two" 3 "three"} [1 2 3])
;=> ("one" "two" "three")

(map :age [{:name "John" :age 31}
           {:name "Sam"  :age 24}
           {:name "Sara" :age 28}])
;=> (31 24 28)

(some #{2 4 6} (range 3 10))
;=> 4

(some {2 "two" 3 "three"} [1 3 2])
;=> "three"

(remove #{5 7} (range 10))
;=> (0 1 2 3 4 6 8 9)

(filter :location [{:name "John" :age 31 :location "NYC"}
                   {:name "Sam"  :age 24}
                   {:name "Sara" :age 28}])
;=> {:name "John", :age 31, :location "NYC"}

여기서 주의할 점은 역시 nil이다. 고차함수의 인자로 사용된 컬렉션에 nil이 있는 경우에 의도한 바와는 다르게 동작할 수가 있다.

(remove #{5 7} (cons nil (range 5)))      ;=> (nil 0 1 2 3 4)
(remove #{5 7 nil} (cons nil (range 5)))  ;=> (nil 0 1 2 3 4)

만일 고차함수의 인자로 사용되는 컬렉션에 nil이 있는지 확실하지 않을 경우에는 contains?를 사용하는 것이 안전하다.

(remove #(contains? #{5 7 nil} %) (cons nil (range 5)))   ;=> (0 1 2 3 4)

이것은 false에 대해서도 마찬지이다.

1.6. 컬렉션 진위 함수(predicates)

다음은 각 컬렉션을 확인하는 진위함수들이다.

(list?  '(1))  ;=> true  (1)
(vector? [1])  ;=> true  (2)
(map?    {1})  ;=> true  (3)
(set?   #{1})  ;=> true  (4)
1 list?는 IPersistentList 구현 여부를 확인한다.
2 vector?는 IPersistentVector 구현 여부를 확인한다.
3 map?은 IPersistentMap 구현 여부를 확인한다.
4 set?은 IPersistentSet 구현 여부를 확인한다.
Table 1. 컬렉션 비교
리스트 벡터 집합

리터럴

'(1 2 3)

[1 2 3]

#{1 2 3}

{:a 1 :b 2}

클래스

clojure.lang. PersistentList

clojure.lang. PersistentVector

clojure.lang. PersistentSet

clojure.lang. PersistentMap

진위 함수

list?

vector?

set?

map?

인터페이스

IPersistentList

IPersistentVector

IPersistentSet

IPersistentMap

생성 함수

(list 1 2 3) ;=> (1 2 3)

(vector 1 2 3) ;=> [1 2 3]

(hash-set 1 2 3) ;=> #{1 2 3}

(hash-map :a 1 :b 2) ;=> {:a 1 :b 2}

cons

(cons 0 '(1 2 3)) ;=> (0 1 2 3)

(cons 0 [1 2 3]) ;=> (0 1 2 3)

(cons 0 #{1 2 3}) ;=> (0 1 2 3)

(cons :b {:a 1}) ;=> (:b [:a 1])

conj

(conj '(1 2 3) 0) ;=> (0 1 2 3)

(conj [1 2 3] 0) ;=> [1 2 3 0]

(conj #{1 2 3} 0) ;=> #{0 1 2 3}

(conj {:a 1} [:b 2]) ;=> {:b 2 :a 1}

1.7. 스택 / 큐 / 트리

리스트, 벡터, 맵, 집합을 보았다. 일반적으로 여러 다른 언어에서는 컬렉션으로 스택과 트리를 지원하지만, 클로저에서는 스택과 트리를 명시적인 컬렉션으로서 구현하여 제공하지는 않는다. 하지만, 리스트와 벡터를 통해서 스택과 트리를 처리할 수 있다.

1.7.1. 스택

리스트와 벡터를 스택으로 사용하기 위해 peekpop 함수를 제공한다.

(conj '(2 1) 1)
;=> (3 2 1)

(peek '(3 2 1))
;=> 3

(pop '(3 2 1))
;=> (2 1)
(conj [1 2] 3)
;=> [1 2 3]

(peek [1 2 3])
;=> 3

(pop [1 2 3])
;=> [1 2]

1.7.2. 큐

클로저는 PersistentQueue를 제공한다. 리터럴이나 명시적 생성함수가 없어서 자바 호출을 직접해야 한다.

(-> (clojure.lang.PersistentQueue/EMPTY)
    (conj 1 2 3)
    pop)
; => (2 3)

1.7.3. 트리

트리는 zipper를 통해 구현된다.

지퍼(Zipper)
트리

내용을 채워넣을 것.

1.8. 정렬

1.8.1. sort와 sort-by

sort 함수는 컬렉션의 아이템을 순서에 따라 정렬한 시퀀스를 반환한다. 비교자(comparator)가 주어지지 않으면, 기본적으로 compare 함수가 사용된다. 만일 컬렉션이 자바 배열이면, 그 순서는 수정된다. 수정을 피하려면 복사본을 사용한다.

(def v [3 4 2 5 1])

(sort v)  ;=> (1 2 3 4 5)

(def ja (to-array v))
(def jac (aclone jo))       ; jac는 ja의 복사본

(seq jac)  ;=> (3 4 2 5 1)
(sort jac) ;=> (1 2 3 4 5)
(seq jac)  ;=> (1 2 3 4 5)  ; 순서가 수정되었다.

(seq ja)   ;=> (3 4 2 5 1)  ; 원래 순서 그대로.

sort 함수에 비교자를 주면 다음과 같이 역순으로 정렬할 수도 있다.

(sort > v)
;=> (5 4 3 2 1)

(sort #(compare %2 %1) v)
;=> (5 4 3 2 1)

compare 함수는 기본으로 사용되는 비교자(Comparator)이다. 두 개의 인수 x와 y를 받는데, x > y 이면 양수, x = y 이면 0, x < y 이면 음수를 반환한다. 이 함수는 수, 문자, 문자열, 키워드, 심볼 등 단순값과 순서있는 컬렉션을 비교한다.

(compare 1 20)            ;=> -1
(compare :a :b)           ;=> -1
(compare "ab" "abc")      ;=> -1
(comapre nil 1)           ;=> -1

(compare [0 1 2] [0 1 2]) ;=> 0
(compare [1 2] [0 1 2])   ;=> -1
(compare [1 2 3] [2 3])   ;=> 1

사실 compare는 내부적으로 java.util.Comparator의 compareTo를 이용한다. 따라서 Comparator 인퍼에이스를 구현한 모든 클래스의 인스턴스에 적용할 수 있다.

sort-by 함수는 sort와 같은데, 첫 인수로 keyfn을 받아 컬렉션의 아이템에 적용해서 반환된 결과를 기준으로 정렬한다.

(sort-by count ["aaa" "bb" "c"])
;=> ("c" "bb" "aaa")

(sort-by val > {:foo 7, :bar 3, :baz 5})
;=> ([:foo 7] [:baz 5] [:bar 3])

(def v [[1 :c] [2 :b] [3 :a]])

(sort-by first v)
;=> ([1 :c] [2 :b] [3 :a])

(sort-by first v)
;=> ([3 :a] [2 :b] [1 :c])

(sort-by second v)
;=> ([3 :a] [2 :b] [1 :c])

(def m [{:id 2 :name "John" :age 31}
        {:id 1 :name "Sam"  :age 24}
        {:id 4 :name "Sara" :age 28}
        {:id 3 :name "David" :age 19}])

(sort-by :age m)
;=> ({:id 3, :name "David", :age 19} {:id 1, :name "Sam", :age 24} {:id 4, :name "Sara", :age 28} {:id 2, :name "John", :age 31})

(sort-by :id > m)
;=> ({:id 4, :name "Sara", :age 28} {:id 3, :name "David", :age 19} {:id 2, :name "John", :age 31} {:id 1, :name "Sam", :age 24})

(sort-by (juxt :id :age) m)
;=> ({:id 1, :name "Sam", :age 24} {:id 2, :name "John", :age 31} {:id 3, :name "David", :age 19} {:id 4, :name "Sara", :age 28})

1.8.2. sorted-map와 sorted-set

sorted-mapsorted-sethashed-maphashed-set과는 달리 `` 정렬된 컬렉션을 만든다.

(sorted-map :z 1 :b 2 :a 3)
;=> {:a 3, :b 2 :z 1}

(into (sorted-map) {:b 2 :a 1})
;=> {:a 1 :b 2}

(apply sorted-map [:b 2 :a 1])
;=> {:a 1 :b 2}

(sorted-set 3 2 1)
;=> #{1 2 3}

(into (sorted-set) [2 3 1])
;=> #(1 2 3)

(apply sorted-set [2 3 1])
;=> #(1 2 3)

1.8.3. sorted-map-by와 sorted-set-by

sorted-map-bysorted-set-by은 비교자를 첫 인수로 받는다는 점만 빼고는, sorted-mapsorted-set 와 같다.

(sorted-map-by > 1 "a", 2 "b", 3 "c")
;=> {3 "c", 2 "b", 1 "a"}

(into (sorted-map-by >)  {1 :a  2 :b  3 :c} )
;=> {3 :c, 2 :b, 1 :a}

(apply (sorted-map-by >)  [1 :a  2 :b  3 :c])
;=> {3 :c, 2 :b, 1 :a}

(sorted-set-by > 3 5 8 2 1)
;=> #{8 5 3 2 1}

(into (sorted-set-by >) [3 5 8 2 1])
;=> #{8 5 3 2 1}

(apply sorted-set-by > [3 5 8 2 1])
;=> #{8 5 3 2 1}

1.8.4. reverse / rseq

reverse 함수는 컬렉션의 순서가 뒤집힌 시퀀스를 lazy가 아니다.

(reverse '(1 2 3))
;=> (3 2 1)

rseq 함수는 컬렉션의 순서를 뒤집힌 시퀀스를 상수 시간에 반환한다. 컬렉션은 clojure.lang.Reversible 인터페이스를 구현한 벡터나 sorted-set, sorted-map 등 이어야 한다. 이것은 reversable? 진위함수로 확인할 수 있다.

(reversible? ())            ;=> false
(reversible? [])            ;=> true
(reversible? {})            ;=> false
(reversible? #{})           ;=> false
(reversible? (sorted-map))  ;=> true
(reversible? (sorted-set))  ;=> true

(rseq '(1 2 3))
;=> java.lang.ClassCastException: clojure.lang.PersistentList cannot be cast to clojure.lang.Reversible

(rseq [1 2 3])
;=> (3 2 1)

(rseq (into (sorted-map) {:a 1 :b 2}))
;=> ([:b 2] [:a 1])

1.8.5. subseq / rsubseq

subseq는 정렬 컬렉션(sorted? 진위함수로 true가 되는 컬렉션: sorted-map과 sorted-set)에서 특정 조건에 맞는 요소들만 추려서 정렬한 시퀀스를 반환한다. rsubseqsubseq와 같은데 역전된 시퀀스를 반환한다. subseqrsubseq의 비교 대상은 컬렉션의 값이 아니라 키이다.

(sorted? (sorted-map))  ;=> true
(sorted? (sorted-set))  ;=> true
(sorted? (sort [1 2]))  ;=> false
;; sorted-map에 적용하는 경우
(def sm (sorted-map :z 37 :x 20 :y 71 :b 8 :a 13 :c 55))

sm
;=> {:a 13, :b 8, :c 55, :x 20, :y 71, :z 37}

(subseq sm <= :c)
;=> ([:a 13] [:b 8] [:c 55])

(subseq sm > :b <= :y)
;=> ([:c 55] [:x 20] [:y 71])

(rsubseq sm > :b <= :y)
;=> ([:y 71] [:x 20] [:c 55])


;; sorted-set에 적용하는 경우
(subseq (sorted-set 1 2 3 4) > 2)
;=> (3 4)
비교함수

compare 함수는 정렬과 관련된 모든 클로저 코어 함수들에서 사용되는 디폴트 비교함수이다. 이 함수는 내부적으로는 java.lang.Comparable 인터페이스의 compareTo 함수를 사용한다. 따라서 Comparable 인터페이스를 구현한 값들은 모두 compare의 대상이 될 수 있다. nil은 가장 낮은 값으로 평가된다.

instance? 함수로 Comparable 인터페이스를 구현했는지 여부를 확인해 볼 수 있다.

(instance? java.lang.Comparable 1)              ;=> true
(instance? java.lang.Comparable :a)             ;=> true
(instance? java.lang.Comparable \a)             ;=> true
(instance? java.lang.Comparable "abcd")         ;=> true
(instance? java.lang.Comparable 'a)             ;=> true
(instance? java.lang.Comparable [])             ;=> true

(instance? java.lang.Comparable ())             ;=> false
(instance? java.lang.Comparable {})             ;=> false
(instance? java.lang.Comparable #{})            ;=> false
(instance? java.lang.Comparable (sorted-map))   ;=> false
(instance? java.lang.Comparable (sorted-set))   ;=> false

만약 compare 함수가 비교할 수 없는 값들이 있다면, 비교 함수를 직접 만들어야 할 것이다. 다음은 비교 함수를 만드는데 필요한 가이드를 제시한다.

우선 compare 함수 자체를 활용해 보는 것이다. 예를 들어 다음은 역전된 순서로 비교한다.

(sort [4 2 3 1])
;=> (1 2 3 4)

(sort #(compare %2 %1) [4 2 3 1])
;=> (4 3 2 1)

다음은 한 번에 여러가지 항목들을 비교한다.

(def john1 {:name "John", :salary 35000.00, :company "Acme" })
(def mary  {:name "Mary", :salary 35000.00, :company "Mars Inc" })
(def john2 {:name "John", :salary 40000.00, :company "Venus Co" })
(def john3 {:name "John", :salary 30000.00, :company "Asteroids-R-Us" })
(def people [john1 mary john2 john3])

(defn by-salary-name-co2 [x y]
  (compare [(:salary y) (:name x) (:company x)]
           [(:salary x) (:name y) (:company y)]))

(pprint (sort by-salary-name-co2 people))
;=> ({:name "John", :salary 40000.0, :company "Venus Co"}
     {:name "John", :salary 35000.0, :company "Acme"}
     {:name "Mary", :salary 35000.0, :company "Mars Inc"}
     {:name "John", :salary 30000.0, :company "Asteroids-R-Us"})

벡터는 길이가 길면 큰 값인데, 길이가 같으면 내부값들 차례로 비교해서 결정한다.

2단 비교 함수

자바의 Comparable 인터페이스는 3단 비교 방식으로, 크면 양의 정수, 작으면 음의 정수, 같으면 0을 반환한다. 하지만 true/false를 반환하는 진위함수를 2단 비교 함수로 사용할 수 있다. 이미 <, >, <=, >= 등의 진위함수들이 사용되는 것을 보았을 것이다. 이런 진위함수는 다음과 같은 방식으로 쉽게 3단 비교가 가능하다. 만약 진위함수가 x와 y를 비교해서 true를 반환하면 -1을, 그렇지 않으면 순서를 바꿔서 다시 y와 x를 비교해서 true를 반환하면 1을, 그렇지 않으면 0을 반환하는 것이다. 아래의 코드가 그 예시를 보여준다.

(if (bool-cmp-fn x y)    ; 처음에는 x y 순서로 비교한다.
  -1     ; x < y
  (if (bool-cmp-fn y x)  ; 다음에는 y x 순서로 비교한다.
    1    ; x > y
    0))  ; x = y

comparator는 2단 비교 함수를 3단 비교 함수로 바꾸어준다.

(def 3-way-less-than (comparator <))

(3-way-less-than 1 2)  ;=> -1
(3-way-less-than 2 1)  ;=> 1
(3-way-less-than 1 1)  ;=> 0

비교함수로 사용되는 클로저의 코어 함수들은 Comparator 인터페이스를 이미 구현했기 때문에, 정렬 관련 작업을 하는 클로저 코어 함수들은 이런 비교함수들을 받아서 내부적으로 2단 비교 함수를 받아서 이런식으로 3단 비교 함수 바꿔서 사용한다.

주의: 값의 일부만 비교하면 문제가 될 수 있다

(defn by-2nd [a b]
  (compare (second a) (second b)))  ; 튜플의 두 번째 요소만 검사한다.

(sorted-set-by by-2nd ["a" 1] ["b" 1] ["c" 1])   (1)
;=> #{["a" 1]}
1 두 번째 요소가 모두 1로 같기 때문에 처음 ["a" 1]이 집합에 들어간 후, 나머지들은 중복이라서 포함되지 못한 것이다.

만일 이때 compare<=로 바꿘주면 포함되기는 하지만 다른 문제가 발행한다.

(defn by-2nd-<= [a b]
  (<= (second a) (second b)))  ; compare 대신 <=로 바꾸었다.

(def sset (sorted-set-by by-2nd-<= ["a" 1] ["b" 1] ["c" 1]))   (1)

sset
;=> #{["c" 1] ["b" 1] ["a" 1]}

(sset ["c" 1])  (2)
;=> nil
(sset ["b" 1])
;=> nil
(sset ["a" 1])
;=> nil
1 ["a" 1]["b" 1] 보다 작다라는 검사에서 true가 되기 때문에, 이제 집합에 포함되기는 한다.
2 하지만 ["c" 1] 끼리 검사할 때도 작다라는 검사에서 true가 되기 때문에, 집합에 값이 있어도 찾지를 못한다.

주의: 비교 함수에 -를 사용해서 값이 차이로 비교할 때 문제가 될 수 있다

compare 함수가 3단 비교로서, 크면 양의 정수, 작으면 음의 정수, 같으면 0을 반환하기 때문에 두 수를 다음과 같이 - 함수를 사용해서 구현할 수도 있을 것이다.

(sort #(- %1 %2) [4 2 3 1])
;=> (1 2 3 4)

이렇게 - 사용하게 되면 예기치 않은 문제가 발생할 수 있다. 예를 들어 다음과 같이 자바 Int 값의 최소값과 최대값이 비교 대상이 되는 경우이다.

(sort #(- %1 %2) [0 1 Integer/MIN_VALUE Integer/MAX_VALUE 3 4])
;=> (0 1 3 4 2147483647 -21474836)

다음으로는 실수를 비교하는 경우이다. 3단 비교함수는 자바의 32비트 Int로 반환하는데, 1보다 작은 실수는 Int로 변환되면서 0으로 캐스팅되기 때문에, 모두 같다고 비교하게 된다.

(sort #(- %1 %2) [1.0 0.9 0.8 0.7])
;=> (1.0 0.9 0.8 0.7)
시퀀스와 nil

클로저에서 nil은 하나의 값으로서 ()(빈 리스트) 과는 다르다. 그러나 때때로 nil()로 취급된다. 이것은 클로저가 처음 만들어질 때 커먼 리스프의 nil의 개념을 그대로 따르다가 나중에는 버린 역사적인 이유 때문이다.

클로저는 처음에는 커먼 리스프의 전통을 따랐는데, 이 전통에 따르면 empty list와 nil은 같은 것이었다. 보다 정확하게 말하자면 커먼 리스프에서는 리스트의 끝을 표시하기 위해 nil을 사용했는데, nil이 리스트의 끝을 의미하게 되면서 자연스럽게 ()도 의미하게 된 것이다. ()는 단지 nil의 별칭이었을 뿐이었지 하나의 값으로 존재하지는 않았다고 보는 것이 맞다. 그래서 클로저도 처음에는 커먼 리스프처럼 nil과 ()을 섞어서 쓰는 함수들이 있엇던 것이다.

하지만 이러한 커먼 리스프의 nil에 대한 전통은 Lazyness를 고려하게 되면 문제가 되었다. 왜냐하면 어떤 것이 리스트인지 아닌지 판별하기 위해서는 first를 적용해 보아야 하는데, 이렇게 되면 항상 처음 요소는 실행한 상태가 되어야 하기 때문이다.

이러한 점을 철저히 조사를 한 후, 클로저는 자신의 시퀀스 개념을 정립/고안하게 되었다. 그것은 empty 상태를 가질 수 있으며, seq를 적용한 이후에야 nil인지 뭔가 내용이 있는지를 확인할 수 있다는 것이다. 하지만 클로저는 하위 호환성을 위해 nil을 empty list처럼 다루는 코드들을 유지하게 된 것이다. cons를 그런 것중 하나이다.

그 전통에서 리스트에 대해 생각한 것은 리스트는 first와 rest를 갖는 그런 것이었다. empty list는 first와 rest가 없기 때문에 리스트가 아니었다. 그런 리스트는 존재할 수 없는 것이다.

하지만 first를 적용해 보지 않고서는 그것이 리스트인지를 판별할 수 없었다. 이러한 이유로 인해서 지연계산을 지원하기가 어렵게 된다. 왜냐하면 무엇인가를 하기 전에 먼저 first를 해봐야 하기 때문이다.

이러한 점을 철저히 조사를 한 후, 클로저는 자신의 시퀀스 개념을 정립/고안하게 되었다. 그것은 empty 상태를 가질 수 있으며, seq를 적용한 이후에야 nil인지 뭔가 내용이 있는지를 확인할 수 있다는 것이다. 하지만 클로저는 하위 호환성을 위해 nil을 empty list처럼 다루는 코드들을 유지하게 된 것이다. cons를 그런 것중 하나이다.

처음 클로저는 커멈 리스프(Common Lisp)의 cons cell에서 시퀀스 개념을 많이 빌려왔다. 커먼 리스프에서는 empty list라는 개념이 없었다. list의 끝을 의미하는 nil만이 있었다. ()는 nil의 별칭(alias)였을 뿐이지, empty list를 의미하는 것이 아니었다. 즉 커먼 리스프에서는 뭔가 내용이 있는 리스프이거나 아무것도 아닌 nil만이 있었지, empty list라는 개념 자체가 없었던 것이다. 아니면 nil은 아무것도 아님을 의미하거마 논리적 거짓을 의미하거나 empty list를 의미했다. 시퀀스는 first와 rest로 된 것이다.

시퀀스 개념은 다른 리스프와 많이 다른 점이다. 클로저는 구체적인 자료구조에 의존하지 않는다. 자료구조들은 모두 시퀀스라는 추상을 통해 접근된다. 클로저는 구체적인 자료구조가 아니라 시퀀스라는 추상을 통해 구현된다. 빈 컬렉션에 대해 시퀀스를 요청하면, 내용이 없기 때문에, 시퀀스를 만들어 낼 수 없어 nil을 반환한다.

the symbol nil is used to represent both the empty list and the ``false'' value for Boolean tests.

커먼 리스프에서 nil은 4가지 역할을 한다.

  • 심볼

  • false

  • empty list

  • don’t care

처음 클로저는 커먼 리스프의 이러한 점을 수용하였다. 그러나 언어에 대한 고민이 깊어지면서 이러한 점이 지연계산(Lazyness)에 문제가 된다는 사실을 발견하게 된다.

(= nil ())   ;=> false

(first ())   ;=> nil
(first nil)  ;=> nil
(rest ())    ;=> ()
(rest nil)   ;=> ()
(cons 1 nil) ;=> (1)

1. 클로저와 다른 리스프 언어, 즉 커먼 리스프나 스킴 등과의 비교는 다음 링크롤 보라. http://clojure.org/lisps
2. construct의 준말
3. conjoin의 준말
4. disjoin의 준말
5. associate의 준말