10개의 데이타 구조에 동작하는 10개의 함수보다는 한 개의 데이타 구조에 동작하는 100개의 함수가 더 좋다.
프로그래밍 경구
지난 장에서는 단순 자료형을 설명하였다. 이번 장에서는 복합 자료형 혹은 컬렉션을 설명한다.
위 경구는 클로저에서 컬렉션, 특히 자료구조(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 | 집합도 맵과 마찬가지로 요소 간에 순서가 없다. |
컬렉션의 요소로 컬렉션이 올 수 있다.
'((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-map
과 hash-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}
리스트는 순차 접근이기 때문에 맨 마지막이 아니라 맨 앞에 추가되는 것이 효율적이다. 반면 벡터는 임의 접근이기 때문에 맨 마지막에 추가되는 것이 효과적이다.
cons 와 conj 는 리스트에 요소를 추가할 때는 똑같이 맨 앞에 추가한다. 하지만 벡터에 요소를 추가할 때는 서로 다르게 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] 는 대상을 지정하기 위한 키들이다. 즉 :a 는 m 에서 {: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
반면 컬렉션에서 특정 요소의 인덱스를 구하려면
|
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을 반환한다.
|
하지만 인덱스가 없는 리스트에는 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 이므로 값이 없음을 의미하지만, 실제로는 인덱스 0 에 nil 이라는 값이 있는 것이다. |
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
맵의 경우, 키들만 뽑아내거나 혹은 값들만 뽑아내는 함수로 keys
와 vals
가 있다.
(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)에 key
와 val
를 적용한 것과 같다.
(map key {:a 1 :b 2 :c 3})
;=> (:a :b :c)
(map val {:a 1 :b 2 :c 3})
;=> (1 2 3)
key
와 val
은 맵의 엔트리에만 적용된다. 맵의 엔트리는 clojure.lang.IPersistentVector를 구현하기 때문에 벡터처럼 표현되기는 하지만, key
와 val
이 벡터에 적용되지는 않는다.
(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이 있는 경우에 의도한 바와는 다르게 동작할 수가 있다.
만일 고차함수의 인자로 사용되는 컬렉션에 nil이 있는지 확실하지 않을 경우에는
이것은 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 구현 여부를 확인한다. |
리스트 | 벡터 | 집합 | 맵 | |
---|---|---|---|---|
리터럴 |
'(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. 스택
리스트와 벡터를 스택으로 사용하기 위해 peek
과 pop
함수를 제공한다.
(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-map
과 sorted-set
은 hashed-map
과 hashed-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-by
와 sorted-set-by
은 비교자를 첫 인수로 받는다는 점만 빼고는, sorted-map
와 sorted-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)에서 특정 조건에 맞는 요소들만 추려서 정렬한 시퀀스를 반환한다. rsubseq
는 subseq
와 같은데 역전된 시퀀스를 반환한다. subseq
와 rsubseq
의 비교 대상은 컬렉션의 값이 아니라 키이다.
(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)