User Tools

Site Tools


lecture:clojure:collections_and_data_structures

컬렉션과 데이타 구조

맵, 벡터, 집합, 리스트는 클로저에서 제공하는 기본 데이타 구조이다. 클로저 데이타 구조는 두가지 특징이 있다.

  1. 데이타 구조는 구체적인 구현이 아닌 추상의 측면에서 가장 우선적으로 사용된다.
  2. 데이타 구조는 변경불가능이면서 존속적이다. 이것을 클로저의 함수형 프로그래밍에 본질적이다.

구현보다는 추상

10개의 데이타 구조에 동작하는 10개의 함수보다는 
한 개의 데이타 구조에 동작하는 100개의 함수가 더 좋다.
               -- Alan J. Perils,  SICP 서문에서

클로저에서는 구체적인 구현 데이타 구조가 4개이지만, 이들이 모두 하나의 추상을 나타내고, 이 추상에 적용되는 100개의 함수 가 더 좋다라는 입장이다. 클로저는 개개의 구현보다는 컬렉션 추상을 더 우선하는데 이것은 자바의 인터페이스나 파이썬, 루비의 연산의 다형성과 유사하기는 하지만, 보다 더 철저하고 강력하다.

다음은 각 함수에 적용된 함수들이다.

list vector set map
def (def lst '(1 2 3)) (def v [1 2 3]) (def s #{1 2 3})) (def m {:a 5 :b 6})
conj (conj lst 0) (conj v 4) (conj s 10) (conj m [:c 7])
seq (seq lst) (seq v) (seq s) (seq m)
into (into lst [4 5] (into v [4 5] (into s [4 5]) (into m [ [4 5] ])

다음은 위에 코드들에 대한 결과값이다.

list vector set map
def #'user/lst #'user/v #'user/s #'user/m
conj (0 1 2 3) [1 2 3 4] #{1 2 3 10) {:a 5 :b 7 :c 6}
seq (1 2 3) (1 2 3) #(1 2 3) ([:a 5] [:b 6])
into (5 4 1 2 3) (1 2 3 4 5) #{1 2 3 4 5} {:a 5 :b 7 4 5}

conj1), seq, into는 clojure.core 함수이다. 위 표는 clojure.core 함수들이 4개의 데이타 구조 (벡터, 리스트, 집합, 맵)에 대해 다형성을 갖고 있음을 보여준다.

conj는 벡터에 대해서는 값을 뒤에 추가하고, 리스트에 대해서는 앞에 추가하며, 집합의 경우에는 같은 원소가 없을 경우에만 추가하고, 맵의 경우에는 키-값 쌍을 추가하는데 같은 키가 있으면 해당 키 값을 갱신한다.

seq는 벡터, 리스트, 집합에 대해서는 그것의 시퀀스 뷰를 만들어내고, 맵에 대해서는 각 키-값 쌍을 벡터 튜플로 바꾼 것들의 시퀀스를 만들어 낸다.

into는 conj와 seq를 결합해서 만든 함수이다. 이 말은 결국 into도 4개의 데이타 구조에 적용될 수 있다는 얘기가 된다. 이런 식으로 작은 함수들을 결합해서 다른 함수를 만들어 내는 것이 클로저의 핵심이다.

클로저의 이러한 다형성으로 인해 새로운 함수를 만들어 추가하는 것은 아주 쉬운 일이 된다. 4개의 데이타 구조는 다음과 같은 부분적 추상성을 제공한다.

  • 컬렉션 추상
  • 시퀀스 추상
  • 연합 추상
  • 인덱스 추상
  • 스택 추상
  • 집합 추상
  • 정렬 추상

컬렉션 추상

컬렉션 추상 함수

클로저의 모든 데이타 구조는 컬렉션 추상을 갖는다. 컬렉션 추상은 다음의 함수를 제공한다.

  • conj : 컬렉션이 새로운 요소를 추가한다.
  • seq : 컬렉션에 대한 시퀀스를 구한다.
  • count : 컬렉션의 요소를 센다.
  • empty : 컬렉션과 같은 타입의 빈 컬렉션을 제공한다.
  • = : 하나 이상의 컬렉션과 같은지를 비교한다.

conj 과 seq 는 이미 보았다. 한 가지 주의할 점은 conj 는 벡터에 대해서는 뒤에 추가하지만, 리스트에 대해서는 앞에 추가한다는 것이다. conj 는 효율적으로 동작하는데, 리스트의 경우 뒤에 추가하게 되면 처음 요소부터 순서대로 마지막 요소까지 거쳐가야 하기 때문에 매우 큰 리스트의 경우 매우 느리게 동작하게 된다. 따라서 리스트의 경우 앞에 추가하는 것이다. 반면 벡터는 인덱스를 갖고 있기 때문에 끝에 추가하는 것은 앞에 추가하는 것 만큼이나 빠르다.

empty 는 다음과 같이 시퀀스 컬렉션에 대해 사용될 수 있다.

(defn swap-pairs [sequential]
  (into (empty sequential)
        (interleave
          (take-nth 2 (drop 1 sequential))
          (take-nth 2 sequential))))
;=> #'user/swap-pairs          
(swap-pairs (apply list (range 10)))
;=> (8 9 6 7 4 5 2 3 0 1)
(swap-pairs (apply vector (range 10)))
;=> [1 0 3 2 5 4 7 6 9 8]

이 코드의 유용성은 파라미터 sequential로 벡터가 오면 벡터로, 리스트가 오면 리스트로 결과를 낸다는 것이다. 이런 다형성은 empty는 해당 타입의 빈 시퀀스를 만들낸다는 사실과 into가 제공하는 다형성 덕분이다.

다음은 맵에 적용되는 경우이다.

(defn map-map [f m]
  (into (empty m)
    (for [[k v] m]
      [k (f v)])))
;=> #'user/map-map
(map-map inc (hash-map :z 5 :c 6 :a 0))
;=> {:z 6, :a 1, :c 7}
(map-map inc (sorted-map :z 5 :c 6 :a 0))
;=> {:a 1, :c 7, :z 6}

파라미터 f는 함수, m은 맵이다. for 루프에서 m의 각 키-값이 [k v]로 하나씩 들어가면서 [k (f v)]를 요소로 하는 리스트가 만들어진다. map-map은 파라미터 m이 정렬 맵이면 정렬 맵으로, 아니면 아닌대로 결과를 내는 다형성을 지닌다.

count는 컬렉션의 요소의 수를 센다.

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

count는 길이가 정의되지 않은 시퀀스를 제외한 다른 컬렉션에 대해서는 효율적으로 동작한다. count는 자바의 Strings, maps, collections, arrays에도 적용될 수 있다.

시퀀스 추상

시퀀스란?

클로저의 시퀀스는 리스프의 cons cell에 해당하는 것이다. 즉 모든 시퀀스는 2개의 쌍으로 이루어져 있는데, 하나는 시퀀스의 첫 요소를 나타내고, 다른 하나는 또 다른 시퀀스이다. 이것은 first와 rest 라는 함수로 얻을 수 있다.

search?q=sequence-view&btnI=lucky

시퀀스 추상 함수

시퀀스 추상은 값들을 순차적으로 접근하는 방법을 제공한다. 다음의 함수들을 제공한다.

  • seq : 주어진 컬렉션에 대해 시퀀스를 만들어 낸다.
  • first, next, rest : 시퀀스에 순차적으로 접근하는 방법을 제공한다.
  • lazy-seq : lazy-seq를 만들어 낸다.

시퀀스 추상이 적용될 수 있는 (seqable) 타입은

  • All Clojure collection types
  • All Java collections (i.e., java.util.*)
  • All Java maps
  • All java.lang.CharSequences, including Strings
  • Any type that implements java.lang.Iterable7
  • Arrays
  • nil (i.e., null as returned from Java methods)
  • Anything that implements Clojure’s clojure.lang.Seqable interface
(seq "Clojure")
;= (\C \l \o \j \u \r \e)
(seq {:a 5 :b 6})
;= ([:a 5] [:b 6])
(seq (java.util.ArrayList. (range 5)))
;= (0 1 2 3 4)
(seq (into-array ["Clojure" "Programming"]))
;= ("Clojure" "Programming")
(seq [])
;= nil
(seq nil)
;= nil

시퀀스를 다루는 많은 함수들이 내부적으로 seq를 호출한다. 예를 들어 map과 set은 String에 대해 seq를 내부적으로 호출한다.

(map str "Clojure")
;= ("C" "l" "o" "j" "u" "r" "e")
(set "Programming")
;= #{\a \g \i \m \n \o \P \r}

이것이 가능한 이유는 first, rest, next 도 String에 직접 적용가능하기 때문이다.

(first "Clojure")
;= \C
(rest "Clojure")
;= (\l \o \j \u \r \e)
(next "Clojure")
;= (\l \o \j \u \r \e)

next는 rest가 아니다

next와 rest는 다르다. 이 차이는 시퀀스에 요소가 하나만 있거나 없을 때 드러난다.

(rest [1])
;= ()
(rest ())
;= ()
(rest nil)
;= ()
(next [1])
;= nil
(next ())
;= nil
(next nil)
;= nil

보다시피 항상 rest는 ()를 next는 nil을 리턴한다. rest와 next의 관계는 다음과 같다.

(= (next x)
   (seq (rest x)))

Seq 라이브러리

클로저의 많은 함수들이 first, next, rest에 기반해서 작성되었다. http://clojure.org/sequences 를 참조하라.

시퀀스는 리스트가 아니다

리스트는 시퀀스이지만, 시퀀스는 리스트가 아니다. 이 둘 사이의 차이점은 다음가 같다.

  • 리스트는 길이를 유지하지만, 시퀀스는 아니다. 그래서 시퀀스의 길이를 구하는데는 시간이 걸린다.
  • 시퀀스의 요소는 접근시에만 평가된다.
  • 시퀀스는 무한할 수 있다.
(let [s (range 1e6)]
  (time (count s)))
; "Elapsed time: 147.661 msecs"
;= 1000000
(let [s (apply list (range 1e6))]
  (time (count s)))
; "Elapsed time: 0.03 msecs"
;= 1000000

range는 lazy seq를 리턴한다. lazy-seq를 세려면 모든 요소를 다 거쳐야 하기 때문에 시간이 오래 걸린다. 반면 리스트는 자신의 길이를 갖고 있기 때문에 시간이 거의 걸리지 않는다.

시퀀스의 내부

시퀀스 추상은 클로저에서 가장 중요한 추상이다. 하지만 시퀀스와 리스트를 구별해야 한다. 벡터, 집합, 맵처럼 리스트는 시퀀스로 보일 수 있지만, 시퀀스가 리스트인 것은 아니다. 시퀀스는 어떤 특정 구현이 아니라 추상이다. 시퀀스는 clojure.lang.ISeq 인터페이스를 구현한 것이라고 말할 수 있다. seq?는 clojure.lang.ISeq를 구현했는지 여부를 판단한다.

(seq? '(1 2))      ;=> true
(seq? [1 2])       ;=> false
(seq? {:foo :bar}) ;=> false
(seq? #{1 2})      ;=> false
(seq? ())          ;=> true

위에서 보면 리스트는 시퀀스인데 벡터, 집합, 맵은 시퀀스가 아닌 것 같다. 위에서 언급했듯이 seq?는 단지 ISeq 인터페이스의 구현 여부만을 판단할 뿐이다. 그리고 위에서는 리스트(그리고 빈 리스트)만 clojure.lang.ISeq를 구현한 것임을 말해줄 뿐이다.

각 데이타 구조의 실제 클래스를 확인해 보자.

(class '(1 2))      ;=> clojure.lang.PersistentList
(class [1 2])       ;=> clojure.lang.PersistentVector
(class {:foo :bar}) ;=> clojure.lang.PersistentArrayMap
(class #{1 2})      ;=> clojure.lang.PersistentHashSet
(class ())          ;=> clojure.lang.PersistentList$EmptyList

클로저 소스 코드를 보면 PersistentList는 ISeq를 구현한 추상 클래스 ASeq를 상속했지만 (EmptyList는 ISeq 바로 구현), PersistentVector, PersistentArrayMap, PersistentHashSet는 ISeq를 구현하지 않는다.

이렇게만 보면 클로저의 데이타 구조는 리스트만 빼고 시퀀스가 아닌 것이다. 물론 구현의 측면에서는 그렇다. 클로저의 데이타 구조가 제공하는 것은 시퀀스가 아니라 시퀀스 추상이다. 리스트를 제외한 다른 데이타 구조도 시퀀스처럼 동작할 수 있는데, 이것을 'seqable'이라고 한다. 반면 'seq'는 ISeq 인터페이스를 구현한 클래스의 객체를 말한다. 리스트를 제외한 클로저의 데이타 구조는 자신은 시퀀스는 아니지만 'seq'를 외부에 제공하기 때문에 'seqable'이 된다.

(seq '(1 2))      ;=> (1 2)
(seq [1 2])       ;=> (1 2)
(seq {:foo :bar}) ;=> ([:foo :bar])
(seq #{1 2})      ;=> (1 2)
(seq ())          ;=> nil

클로저 데이타 구조의 seq의 실제 구현 클래스를 확인해 보자.

(class (seq '(1 2))       ;=> clojure.lang.PersistentList
(class (seq [1 2]))       ;=> clojure.lang.PersistentVector$ChunkedSeq
(class (seq {:foo :bar})) ;=> clojure.lang.PersistentArrayMap$Seq
(class (seq #{1 2}))      ;=> clojure.lang.PersistentHashSet$KeySeq
(class (seq ()))          ;=> nil

리스트의 경우는 자신과 그 seq가 같은 클래스이지만, 다른 데이타 구조의 seq는 각자의 내부 클래스이다. 이 내부 클래스들이 해당 데이타 구조에 대한 시퀀스 관점을 제공하는 것이다.


레이지(Lazy) 시퀀스

시퀀스의 내용이 뒤늦게(Lazy) 평가되는 것이 가능한데, 이렇게 되면 요소의 값은 처음에는 존재하지 않다가 사용자가 그 요소에 접근할 때만 계산되어 생겨나는 것이다. 이 값들은 한 번반 계산되면 계속 유지되는데, 이렇게 레이지 시퀀스에서 요소에 접근하면서 값이 계산되는 것을 '실현'이라고 하고, 모든 값이 실현되면 '완전 실현'되었다고 한다.

lazy-seq2) 함수를 사용하여 레이지 시퀀스를 만드는데, 인자는 시퀀스로 평가될 수 있는 문구다.

(defn random-ints [limit]
"Returns a lazy seq of random integers in the range [0,limit)."
  (lazy-seq
    (println "realizing random number")
    (cons (rand-int limit)
      (random-ints limit))))

lazy-seq를 사용하는 방법은 다음과 같다. cons로 묶을 2개를 생각한다. cons의 인자가 되어야 하니, 당연히 첫번째 인자는 어떤 값이 되어야 하고 두번째 인자는 시퀀스여야 한다. 그래서 첫번째 인자는 레이지 시퀀스에 접근할 때 평가되어 값이 나오는 문구가 되어야 하고, 두번째 인자는 다시 자기 자신을 재귀 호출하면 된다. 왜냐하면 자기 자신이 바로 lazy-seq에 의해 시퀀스가 되기 때문이다.

(def rands (take 10 (random-ints 50)))  ;;; 1.
;= #'user/rands
(first rands)                           ;;; 2.
; realizing random number
;= 39
(first rands)                           ;;; 3.
;= 39
(second rands)                          ;;; 4.
; realizing random number
;= 17
(nth rands 3)                           ;;; 5.
; realizing random number
; realizing random number
;= 44
(count rands)                           ;;; 6.
; realizing random number
; realizing random number
; realizing random number
; realizing random number
; realizing random number
;= 10
(count rands)                           ;;; 7.
;= 10

lazy-seq 문구가 평가될 때마다 프린트문이 출력되도록 random-ints함수에 println을 사용하였다.

  1. 최초 레이지 시퀀스를 만들어 rands라는 Var 변수에 넣는다. 아직 접근이 없기 때문에 어떤 평가도 없다.
  2. first로 처음 요소에 접근하기 때문에 평가가 이루어지고, 프린트문이 출력되며, 39라는 결과를 얻는다. 이 값은 캐쉬된다.
  3. 두번 째 first 호출은 이미 평가된 요소에 접근하기 때문에 평가는 수행되지 않고, 캐쉬된 값 39가 리턴된다.
  4. second로 두 번째 요소에 접근하기 때문에 평가가 이루어지고, 프린트문이 출력되며, 17라는 결과를 얻는다.
  5. 네번 째 요소에 접근하고 있는데, 중간 요소를 거쳐야 하느데, 세번째와 네번째 요소만 평가되기 때문에 프린트문이 2번 출력됨을 확인할 수 있다. 값은 44.
  6. 레이지 시퀀스의 요소를 갯수를 알기 위해서는 전체 요소를 다 세어봐야 하기 때문에 모든 요소가 평가된다.
  7. 이미 모든 요소에 평가가 이우러져 있기 때문에 프린트문이 출력되지 않고 있다.

이것을 그림으로 그리면 다음과 같다. 최초 레이지 시퀀스가 만들어지면 다음과 같다.

search?q=lazy-seq-1.svg&btnI=lucky

lazy-seq에는 아직 아무 값도 존재하지 않는다. 사각형은 접근시에 평가가 될 것이다. 하지만 후속 요소는 아직 존재하지 않는다. 이것을 점선 원으로 표시했다.

첫 번째 요소에 접근하면 다음과 같다.

search?q=lazy-seq-2.svg&btnI=lucky

첫 요소에 접근하면 사각형으로 된 평가문이 평가된다. 이 값은 0으로 결과가 되어 첫 요소로서 캐쉬된다. 다음은 역시 또 레이지 시퀀스가 된다.

cons와 list*는 마지막에 오는 인자를 반드시 평가하는 것은 아니다. 다시 말해 cons와 list*의 결과인 새로운 seq는 그 rest가 평가되지 않은 채 lazy-seq 에 전달되는데, lazy-seq은 전달받은 seq의 rest의 평가를 유보한다.

레이지 시퀀스에 접근시마다 평가되는 값이 반드시 하나일 필요는 없다. 임의 갯수의 요소를 평가하고 싶으면 list*를 사용하면 된다. 다음 코드는 접근시마다 2개의 값을 평가하도록 random-ints를 수정한 것이다.

(defn random-ints [limit]
"Returns a lazy seq of random integers in the range [0,limit)."
  (lazy-seq
    (println "realizing random number")
    (list* (rand-int limit) (rand-int limit)
      (random-ints limit))))
 
(def rands (take 10 (random-ints 50)))
;= #'user/rands
(first rands)                      ;; 1. 
; realizing random number
;= 16
(first rands)                      ;; 2.
;= 16
(second rands)                     ;; 3.
;= 33
(nth rands 2)                      ;; 4.
; realizing random number
;= 47
(nth rands 3)                      ;; 5.
;= 5
  1. 첫 요소에 대한 접근을 인해 평가가 수행되어 프린트문이 출력되었다.
  2. 다시 첫 요소에 접근하고 있는데, 이미 평가되었기 때문에 프린트문은 없다.
  3. 두 번째 요소에 접근하고 있는데, 출력된 프린트문이 없다. 첫 요소 접근이 이미 두번째 요소가 생겨났기 때문이다.
  4. 세 번째 요소에 접근하면서 새로 평가가 수행되어 프린트문이 출력된다.
  5. 네 번째 요소는 이미 생겨났기 때문에 역시 프린트문이 없다.

random-ints는 사실 repeatedly 시퀀스 라이브러리 함수로 간단하게 대체된다.

(repeatedly 10 (partial rand-int 50))
;=> (47 19 26 16 39 5 30 13 9 43)

레이지 시퀀스를 사용할 때 next와 rest는 차이가 있다.

(def x (next (random-ints 50)))
; realizing random number
; realizing random number
 
(def x (rest (random-ints 50)))
; realizing random number

next는 다음 요소를 체크하기 때문에, 첫 요소와 두 번째 요소가 접근되면서 프린트문이 2개 출력되었다. 반면 rest는 첫 요소만 접근하고 나머지는 그대로 리턴하기 때문에 출력된 프린트문은 하나이다.

이러한 사정은 인수분해도 마찬가지인데, 인수분해는 항상 rest가 아닌 next를 사용한다.

(let [[x & rest] (random-ints 50)])
; realizing random number
; realizing random number
;= nil

레이지 시퀀스를 강제로 실현하기 위해서는 dorun이나 doall을 사용한다.

(dorun (take 5 (random-ints 50)))
; realizing random number
; realizing random number
; realizing random number
; realizing random number
; realizing random number
;= nil

dorun은 레이지 시퀀스를 결과를 유지하지 않는다. 반면 doall3)은 유지한다. dorun은 단지 부수효과(side-effecting)만을 원할 때 사용한다.

레이지 시퀀스를 정의하는 코드는 부수효과를 최소화해야 한다

레이지 시퀀스의 값은 정의될 때가 아니라 접근될 때 실현되기 때문에, 부수효과가 언제 어디서 생겨나게 되는지를 따라가는 것이 결코 쉽지 않다. 더욱이 특정 데이타 구조의 구현은 요소에 대한 접근을 하나씩 순차적으로 하는 것이 아니라 성능을 위해 내부적으로 한 번에 몰아서 하기도 하기 때문에 문제는 더 복잡해진다.

이러한 이유로 레이지 시퀀스를 결코 흐름 제어용으로 사용해서는 않된다. 클로저의 레이지 시퀀스는 결코 그런 용도로 설계된 것이 아니다.

머리 잡기 (Head retention)

레이지 시퀀스가 한 번 평가되면 그 값은 캐쉬된다고 했다. 이 값은 계속 유지되기 때문에 가비지 컬렉션되지 않는다. 이것을 머리 잡기 (head retention)이라고 하는데, VM에 성능상의 문제를 일으키며, 메모리 고갈을 초래한다.

split-with 함수는 다음과 같다.

(split-with neg? (range -5 5))
;= [(-5 -4 -3 -2 -1) (0 1 2 3 4)]
(let [[t d] (split-with #(< % 12) (range 1e8))]
[(count d) (count t)])
;= #<OutOfMemoryError java.lang.OutOfMemoryError: Java heap space>
(let [[t d] (split-with #(< % 12) (range 1e8))]
[(count t) (count d)])
;= [12 99999988]

연합 추상

연합 추상 함수

연합 추상은 키와 값을 연결짓는 방식을 제공한다. 다음의 함수가 있다.

  • assoc : 컬렉션에 키-값에 대한 신규 연합을 만든다. (맵만 사용 가능)
  • dissoc : 컬렉션에서 키-값 연합을 해제한다. (맵만 사용 가능)
  • get : 컬렉션에서 해당 키에 대한 값을 구한다.
  • contains? : 컬렉션에서 해당 키에 대한 값이 있는지 판단한다.

맵은 conj나 seq 함수로는 키값쌍에 대한 컬렉션으로 보이지만, 연합 추상 함수에 더 적절할 컬렉션이다.

(def m {:a 1, :b 2, :c 3})
;= #'user/m
(get m :b)
;= 2
(get m :d)
;= nil
(get m :d "not-found")
;= "not-found"
(assoc m :d 4)
;= {:a 1, :b 2, :c 3, :d 4}
(dissoc m :b)
;= {:a 1, :c 3}
(assoc m
:x 4
:y 5
:z 6)
;= {:z 6, :y 5, :x 4, :a 1, :c 3, :b 2}
(dissoc m :a :c)
;= {:b 2}

get과 assoc은 벡터도 지원하는데, 좀 반직관적이지만 벡터로 연합 추상 컬렉션이다. 벡터의 경우 키는 인덱스이다.

(def v [1 2 3])
;= #'user/v
(get v 1)
;= 2
(get v 10)
;= nil
(get v 10 "not-found")
;= "not-found"
(assoc v
1 4
0 -12
2 :p)
;= [-12 4 :p]

집합은 값이 키가 되는 맵이라고 볼 수 있다.

(get #{1 2 3} 2)
;= 2
(get #{1 2 3} 4)
;= nil
(get #{1 2 3} 4 "not-found")
;= "not-found"

contains?

contains?는 컬렉션이 해당키가 있는지 여부를 판단한다.

(contains? [1 2 3] 0)
;= true
(contains? {:a 5 :b 6} :b)
;= true
(contains? {:a 5 :b 6} 42)
;= false
(contains? #{1 2 3} 1)
;= true

contains?는 값이 아니라 키의 존재 여부를 확인하는 것이라는 것에 주의해야 한다.

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

값이 nil일 때 주의

get은 해당키에 대한 값이 없을 때 nil을 리턴한다. 하지만 nil이 해당키에대한 값인 경우가 있다.

(get {:ethel nil} :lucy)
;= nil
(get {:ethel nil} :ethel)
;= nil

이럴 땐 find를 사용할 수 있다.

(find {:ethel nil} :lucy)
;= nil
(find {:ethel nil} :ethel)
;= [:ethel nil]

find는 특히 if-let이나 when-let과 잘 맞는다.

(if-let [e (find {:a 5 :b 6} :a)]
(format "found %s => %s" (key e) (val e))
"not found")
;= "found :a => 5"
(if-let [[k v] (find {:a 5 :b 6} :a)]
(format "found %s => %s" k v)
"not found")
;= "found :a => 5"

인덱스 추상

인덱스는 포인터와 비슷하다. 인덱스는 언어에 복잡성을 끌고 들어온다. 하지만 특별한 경우에는 필요할 때가 있는데 이럴 때를 위해 인덱스 추상이 있는 것이다.

인덱스 추상 함수

인덱스 추상 함수는 nth 하나 뿐이다.

  • nth : get의 특수형. get이 nil을 리턴할 때 nth는 예외를 던진다.
(nth [:a :b :c] 2)
;= :c
(get [:a :b :c] 2)
;= :c
(nth [:a :b :c] 3)
;= java.lang.IndexOutOfBoundsException
(get [:a :b :c] 3)
;= nil
(nth [:a :b :c] -1)
;= java.lang.IndexOutOfBoundsException
(get [:a :b :c] -1)
;= nil

기본값을 줄수 있다는 것은 같다.

(nth [:a :b :c] -1 :not-found)
;= :not-found
(get [:a :b :c] -1 :not-found)
;= :not-found

컬렉션이 아닌 경우에도 차이가 있다.

(get 42 0)
;= nil
(nth 42 0)
;= UnsupportedOperationException nth not supported on this type: Long  clojure.lang.RT.nthFrom

스택 추상

스택 추상은 후입선출을 지원한다. 클로저는 명시적인 스택은 지원하지는 않지만 다음의 세가지 함수를 통해 스택 추상을 제공한다.

스택 추상 함수

  • conj : 스택 꼭대기에 값을 넣는다.
  • pop : 스택 꼭대기 값을 제외한 새로운 스택을 반환한다.
  • peek : 스택 꼭대기에서 값을 구한다.

다음은 벡터를 스택으로 사용하는 예이다.

(conj [] 1)
;= [1]
(conj [1 2] 3)
;= [1 2 3]
(peek [1 2 3])
;= 3
(pop [1 2 3])
;= [1 2]
(pop [1])
;= []

다음은 리스트를 스택으로 사용하는 예이다.

(conj '() 1)
;= (1)
(conj '(2 1) 3)
;= (3 2 1)
(peek '(3 2 1))
;= 3
(pop '(3 2 1))
;= (2 1)
(pop '(1))
;= ()

집합 추상

집합 추상 함수

  • disj : 집합에서 요소를 뺀다.
(disj #{1 2 3} 3 1)
;= #{2}

정렬 추상

정렬 추상은 특정 비교 함수에 의해 정해진 순서에 따라 요소들을 정렬하는 방식을 말한다.

정렬 추상 함수

다음은 정렬 추상이 지원하는 함수이다.

  • rseq : 역순서의 시퀀스를 리턴한다. 이것은 상수 시간에 이루어진다.
  • subseq : 서브시퀀스를 리턴한다.
  • rsubseq : subseq와 같으나 순서가 반대이다.

맵과 집합만이 정렬 추상이 가능하다. 이것을 만들기 위한 리터럴 표기법은 없다. sorted-map과 sorted-set, sorted-map-by와 sorted-set-by를 사용해야 한다.

(def sm (sorted-map :z 5 :x 9 :y 0 :b 2 :a 3 :c 4))
;= #'user/sm
sm
;= {:a 3, :b 2, :c 4, :x 9, :y 0, :z 5}
(rseq sm)
;= ([:z 5] [:y 0] [:x 9] [:c 4] [:b 2] [:a 3])
(subseq sm <= :c)
;= ([:a 3] [:b 2] [:c 4])
(subseq sm > :b <= :y)
;= ([:c 4] [:x 9] [:y 0])
(rsubseq sm > :b <= :y)
;= ([:y 0] [:x 9] [:c 4])

rseq는 역순서의 정렬을 하지만 걸리는 시간은 상수이다. sorted-map은 일반 맵보다는 성능면에서 훨씬 빠르다. reserse는 요소의 갯수에 비례하는 시간이 걸린다.

compare는 정렬을 위한 기본 비교 함수이다. 오름차순으로 정렬되는데, 클로저의 모든 스칼라와 시퀀스 컬렉션에 적용된다.

(compare "ab" "abc")
;= -1
(compare ["a" "b" "c"] ["a" "b"])
;= 1
(compare ["a" 2] ["a" 2 0])
;= -1
(compare 2 2)
;= 0

compare는 사실 java.lang.Comparable을 구현한 클래스라면 어떤 것이라도 적용된다 : Boolean, keyword, symbol이나 기타 Comparable 인터페이스를 구현한 클래스들.

비교 함수와 판단 함수

정렬 추상은 판단 함수와 함께 사용된다. 비교 함수(comparator)는 2개의 인자를 받는 함수인데, 첫 인자가 크면 1, 작으면 -1, 둘 다 같으면 0을 리턴한다. sorted-map과 sorted-set은 clojure.core/compare가 기본 비교 함수이다. 하지만 sorted-map-by와 sorted-set-by는 사용자가 비교 함수를 지정할 수 있다.

비교 함수는 java.util.Comparator를 구현한 것이다. 비교 함수는 이 인터페이스를 직접 구현하기 보다는 보통 비교 함수는 판단 함수를 이용해서 만들 수 있다. 판단 함수(predicate4))는 참과 거짓을 판단하는 함수이다. 즉 비교 함수의 2 인자에 대해 판단함수가 참이면 1, 거짓이면 2 인자의 순서를 바꾸어서 판단 함수가 참이면 -1, 아니면 0을 리턴하면 된다. 그래서 비교 함수는 보통 판단 함수의 구성을 통해 쉽게 만드는 것이 가능하다.

sort와 sort-by 함수를 통해 예를 확인해 보자.

(sort < (repeatedly 10 #(rand-int 100)))
;= (12 16 22 23 41 42 61 63 83 87)
(sort-by first > (map-indexed vector "Clojure"))
;= ([6 \e] [5 \r] [4 \u] [3 \j] [2 \o] [1 \l] [0 \C])

다음은 sorted-map-by에 비교 함수를 적용된 예이다.

(sorted-map-by compare :z 5 :x 9 :y 0 :b 2 :a 3 :c 4)
;= {:a 3, :b 2, :c 4, :x 9, :y 0, :z 5}
(sorted-map-by (comp - compare) :z 5 :x 9 :y 0 :b 2 :a 3 :c 4)
;= {:z 5, :y 0, :x 9, :c 4, :b 2, :a 3}

(comp - compare)는 compare의 결과에 -를 한다. (comp f g)는 f(g)이다.

컬렉션 사용법

컬렉션에서 데이타를 뽑아내는데, 특히 연합 추상의 경우, get가 nth를 사용하는 것은 좀 번거롭다. 다행히 클로저의 컬렉션은 그 자체로 함수이고, 키가 컬렉션과 같이 쓰이면 키도 함수가 된다.

컬렉션은 함수다

(get [:a :b :c] 2)
;= :c
(get {:a 5 :b 6} :b)
;= 6
(get {:a 5 :b 6} :c 7)
;= 7
(get #{1 2 3} 3)
;= 3

이것은 다음과 같이 간결해질 수 있다.

([:a :b :c] 2)
;= :c
({:a 5 :b 6} :b)
;= 6
({:a 5 :b 6} :c 7)
;= 7
(#{1 2 3} 3)
;= 3

컬렉션 키는 함수다

키워드와 심볼이 키일 때 함수가 될 수 있다. 하지만 수는 않된다.

(get {:a 5 :b 6} :b)
;= 6
(get {:a 5 :b 6} :c 7)
;= 7
(get #{:a :b :c} :d)
;= nil
(:b {:a 5 :b 6})
;= 6
(:c {:a 5 :b 6} 7)
;= 7
(:d #{:a :b :c})
;= nil

컬렉션보다는 키

그렇다면 컬렉션과 키중 어떤 것을 함수로 사용하는 것이 좋을까? 여러 가지 면에서 키가 좋다. 다음 예를 보자

(defn get-foo [map]
  (:foo map))
;= #'user/get-foo
(get-foo nil)
;= nil
(defn get-bar [map]
  (map :bar))
;= #'user/get-bar
(get-bar nil)
;= #<NullPointerException java.lang.NullPointerException>

컬렉션의 경우 예외를 발생하지만, 키는 단지 nil을 리턴한다. 게다가 위 코드에서 map이 실제 컬렉션이 아닌 경우에도 예외가 발생할 것이다. 물론 키가 키워드나 심볼이 아니고 스트링이나 수이면 컬렉션을 함수로 사용하는 것이 좋을 것이다.

고계함수에 컬렉션과 키 사용하기

키워드와 심볼 그리고 컬렉션이 함수이기 때문에 고계함수에 사용될 수 있는데, 아주 편리하다.

(map :name [{:age 21 :name "David"}
{:gender :f :name "Suzanne"}
{:name "Sara" :location "NYC"}])
;= ("David" "Suzanne" "Sara")

위 코드에서 :name 키워드가 함수처럼 사용되어서 맵에서 이름을 추출하는 역할을 하고 있다.

some은 시퀀스에서 주어진 판단 함수를 만족하는 첫 요소를 찾는데, 판단 함수가 집합이면 특정 값들의 존재 여부 판단에 아주 편리하다.

(some #{1 3 7} [0 2 4 5 6])
;= nil
(some #{1 3 7} [0 2 3 4 5 6])
;= 3

filter는 시퀀스에서 주어진 판단 함수를 만족하는 요소만 남기는데, 역시 판단 함수에 키워드나 컬렉션을 쓸 수 있다.

(filter :age [{:age 21 :name "David"}
              {:gender :f :name "Suzanne"}
              {:name "Sara" :location "NYC"}])
;= ({:age 21, :name "David"})
 
(filter (comp (partial <= 25) :age) [{:age 21 :name "David"}
                                     {:gender :f :name "Suzanne" :age 20}
                                     {:name "Sara" :location "NYC" :age 34}])
;= ({:age 34, :name "Sara", :location "NYC"})

remove는 정확히 filter와 반대인데, 시퀀스에서 판단 함수를 만족하는 요소만 제거한다. 즉 remove는 (filter (complement f) collection)과 같다.

nil과 false를 주의하라

만약 nil이나 false가 시퀀스 안에 요소로 있다면, 판단 함수로는 그 존재 여부를 확인할 수 없다.

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

시퀀스에 nil이나 false가 없다는 확신이 서지 않는다면, contains?를 사용하라.

(remove (partial contains? #{5 7 false}) (cons false (range 10)))
;= (0 1 2 3 4 6 8 9)

데이타 구조 타입

리스트

리스트는 클로저 컬렉션 타입중 가장 단순하다. 리스트 가장 중요한 역할은 함수 호출이다. 하지만 보통 소스 파일에서 리터럴로 많이 사용한다.

리스트는 단일 연결 리스트여서 임의접근이 않되고, 헤드에서 수정과 접근이 이루어지는데, conj는 헤드에 새 값을 추가하고, pop과 rest는 헤드 뒤의 하위 리스트를 구한다. nth를 하면 크기에 비례해서 시간이 걸리는 반면, get은 아예 적용되지 않아 nil을 리턴한다.

list는 리스트를 만드는 함수이고, list?는 리스트 여부를 확인하는 함수이다.

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

벡터

벡터는 랜덤 접근과 변경이 되는 시퀀스인데, 자바의 java.util.ArrayList, 파이썬의 리스트, 루비의 array가 비슷하다. 벡터는 시퀀스이면서 연합 추상, 인덱스 추상, 스택 추상을 제공한다. 벡터 리터럴이외에 vec과 vector로 베터를 만들 수 있다.

(vector 1 2 3)
;=> [1 2 3]
(vec (range 5))
;=> [1 2 3 4 5]

vector?는 list?와 마찬가지로 vector임을 판단하는 함수이다.

Primitive 벡터

클로저는 primitive 타입을 요소로 하는 벡터를 만들 수 있다. vector-of에 :int, :long, :float, :double, :byte, :short, :boolean, :char 를 사용하면 빈 벡터를 리턴한다.

(into (vector-of :int) [Math/PI 2 1.3])
;=> [3 2 1]
(into (vector-of :char) [101 101 102])
;=> [\d \e \f]
(into (vector-of :int) [1 2 7574969923846818643424u327]
;=> java.lang.IllegalArgumentException: Value out of range for int: ...

튜플로서의 벡터

벡터는 튜플로 사용되기 딱 좋다. 여러 개의 값이 그룹이 되는 경우 튜플로 처리하면 좋다. 예를 들어 여러 개의 값을 리턴하는 경우이다.

(defn euclidian-division [x y]
[(quot x y) (rem x y)])
(euclidian-division 42 8)
;= [5 2]

하지만 튜플은 다음과 같은 약점이 있다.

  • 튜플은 자기설명적이지 않다. 즉 각 자리마다 무슨 값을 갖는지 기억해야 한다.
  • 튜플은 유연하지 않다. 중간의 값이 없어도 그 자리에 값을 할당해야 한다.

위와 같은 약점이 부각될 때는 맵을 쓰는 것이 좋겠다. 하지만 튜플이 확실하게 좋은 것들이 있다.

(def point-3d [42 26 -7])
(def travel-legs [["LYS" "FRA"] ["FRA" "PHL"] ["PHL" "RDU"]])

집합

다음은 집합의 리터럴을 만드는 예이다.

#{1 2 3}
;= #{1 2 3}
#{1 2 3 3}
;= #<IllegalArgumentException java.lang.IllegalArgumentException:
;= Duplicate key: 3>

집합은 중복이 않되기 때문에 위의 코드처럼 3이 중복되면 예외가 발생한다.

hash-set으로 집합을 만들 수 있다.

(hash-set :a :b :c :d)
;= #{:a :c :b :d}

집합이 시퀀스에 대해 판단 함수로 사용되면 표현이 매우 간결해진다.

(apply str (remove (set "aeiouy") "vowels are useless"))
;= "vwls r slss"
(defn numeric? [s] (every? (set "0123456789") s))
;= #'user/numeric?
(numeric? "123")
;= true
(numeric? "42b")
;= false

다음은 맵의 리터럴을 만드는 예이다.

{:a 5 :b 6} ;= {:a 5, :b 6} {:a 5 :a 5} ;= #<IllegalArgumentException java.lang.IllegalArgumentException: ;= Duplicate key: :a>

맵은 키가 중복되면 않되기 때문에 위의 코드에서처럼 예외가 발생한다.

hash-map으로 맵을 만들 수 있다.

(hash-map :a 5 :b 6) ;= {:a 5, :b 6} (apply hash-map [:a 5 :b 6]) ;= {:a 5, :b 6}

keys와 vals

keys는 맵의 키만으로 된 시퀀스를, vals는 맵의 값으로만 된 시퀌스를 리턴한다.

(keys m)
;= (:a :b :c)
(vals m)
;= (1 2 3)

맵 요소에 대한 함수 key와 val도 있다.

(map key m)
;= (:a :c :b)
(map val m)
;= (1 3 2)

불변성과 존속성

클로저의 데이타는 불변이다. 하지만 그냥 불변이 아니다. 존속적 불변이다. 이것이 클로저의 가장 뛰어난 점중 하나이다.

(def v (vec (range 1e6)))
;= #'user/v
(count v)
;= 1000000
(def v2 (conj v 1e6))
;= #'user/v2
(count v2)
;= 1000001
(count v)
;= 1000000

위 코드에서 v는 0부터 999999까지의 정수 벡터이다. 그리고 v2는 v에 1000000을 추가했다. 하지만 (count v)가 여전히 1000000인 것을 보면 역시 v는 변하지 않았고, v2는 1000001개임을 알 수 있다. 그렇다면 v2는 v를 복사한 다음 1000000을 추가한 것인가? 만일 불변 데이타가 이렇게 비효율적이라면 사용하지 못했을 것이다.

클조저는 아주 효율적으로 불변성을 지원하는데, 이 특징을 존속성(Persistence)라고 한다.

존속성과 구조적 공유

클로저의 컬렉션은 불변이지만 매우 효율적인 변경이 가능한데, 때로는 자바 만큼이나 빠르기도 하다. 이것은 컬렉션이 자신의 구조를 거의 바꾸지 않으면서 변경이 최소한의 연산만으로 이루어지기 때문이다. 이것을 구조적 공유라고 하는데, 깊은 복사를 하지 않고 새로은 레퍼런스가 기존 구조를 공유하면서 차이나는 부분만 추가하는 것을 말한다.

  • 해쉬맵 : hash array mapped tries
  • 벡터 : array mapped hash trie
  • 집합 : 해쉬맵에 기반
  • 순서 집합과 맵 : red-black trees

이러한 데이타 구조는 상당히 빠른데, O(log32 n) 정도이고, 순수 컬렉션의 경우 O(log2 n)이다.

트랜션트

순수 함수는 불변 데이타를 입력받고, 불변 데이타를 출력한다. 하지만 순수 함수가 로컬 변수를 수정하고 나중에는 불변 데이타를 리턴한다면 아무 문제가 없다. “숲에 나무가 쓰러져도 소리가 나지 않는다면 누가 알까?”

트랜션트(transient)는 변경가능 컬렉션이다. 존속성 컬렉션과는 달리 트랜션트 컬렉션은 변경 이전 상태가 보장되지 않는다.

(def x (transient []))
;= #'user/x
(def y (conj! x 1))
;= #'user/y
(count y)
;= 1
(count x)
;= 1

존속성 컬렉션이 빠르긴 하지만 매 갱신시마다 새로운 메모리를 할당하는 부하는 어쩔수가 없다. 수백 수천개의 값을 conj될 때는 이것은 상당한 성능 저하를 야기하게 된다. 트랜션트는 이런 경우를 위한 최적화 솔루션이다. 예를 들어 다음과 같이 해보자.

(defn vrange [n]
  (loop [i 0 v []]
    (if (< i n)
      (recur (inc i) (conj v i))
      v)))
;=> #'user/vrange
 
(defn vrange2 [n]
  (loop [i 0 v (transient [])]
    (if (< i n)
      (recur (inc i) (conj! v i))
      (persistent! v))))
;=> #'user/vrange2
 
(time (def v (vrange 1000000)))
;>> "Elapsed time: 297.444 msecs"
;=> #'user/v
 
(time (def v2 (vrange2 1000000)))
"Elapsed time: 34.428 msecs"
;=> #'user/v2

트랜션트를 이용하면 훨씬 빠르다는 것을 확인할 수 있다.

벡터와 비순서 맵만이 트랜션트가 있다. 순서 집합에는 없다. 컬렉션이 트랜션트를 지원하는지는 clojure.lang.IEditableCollection 인터페이스를 확인해 보면 된다.

(defn transient-capable? [coll]
  "Returns true if a transient can be obtained for the given collection.
   i.e. tests if `(transient coll)` will succeed."
  (instance? clojure.lang.IEditableCollection coll))

존속성 컬렉션으로 트랜션트 컬렉션을 만들어도 원래의 존속성 컬렉션은 아무런 영향을 받지 않는다.

(def v [1 2])
;= #'user/v
(def tv (transient v))
;= #'user/tv
(conj v 3)
;= [1 2 3]

하지만 트랜션트 컬렉션을 다시 존속성 컬렉션으로 바꾸면 원랙의 트랜션트 컬렉션은 더 이상 유효하지 않게 된다.

(persistent! tv)
;= [1 2]
(get tv 0)
;= #<IllegalAccessError java.lang.IllegalAccessError:
;= Transient used after persistent! call>

transient와 persistent!는 상수 시간에 리턴된다. 트랜션트도 다음과 같은 많은 접근 함수가 있다.

(nth (transient [1 2]) 1)
;= 2
(get (transient {:a 1 :b 2}) :a)
;= 1
((transient {:a 1 :b 2}) :a)    ;;; 트랜션트도 함수다!
;= 1
((transient [1 2]) 1)
;= 2
(find (transient {:a 1 :b 2}) :a)
;= #<CompilerException java.lang.ClassCastException:
;= clojure.lang.PersistentArrayMap$TransientArrayMap
;= cannot be cast to java.util.Map (NO_SOURCE_FILE:0)>

seq는 트랙션트가 될 수 없다. 왜냐하면 seq는 그 소스인 컬렌션보다 더 지속될 수 있기 때문에 seq가 스스로 만든 존속 컬렉션이라는 보장에 의존할 수 없기 때문이다.

(transient [1 2])
;=> #<TransientVector clojure.lang.PersistentVector$TransientVector@4693f9>
(transient (seq [1 2]))
;=> ClassCastException clojure.lang.PersistentVector$ChunkedSeq cannot be cast to clojure.lang.IEditableCollection

트랜션트는 다른 컬렉션 갱신 함수와 사용할 수 없고 전용의 함수가 따로 있다. conj!, assoc!, dissoc!, disj!, pop! 등이다. 트랜션트 함수는 모두 느낌표가 뒤에 붙는데, 이것은 이 함수들이 트랜션트 컬렉션에 대한 무효한 행동을 나타내기 위함이다. 이 함수들이 트랜션트 컬렉션에 일단 한 번 적용되면, 그 컬렉션은 다시는 건드려지면 않된다 읽기만 하는 것도 않된다). conj, assoc, disj의 결과를 사용해야 효과를 보는 것처럼, 이들 함수도 반드시 그 결과가 사용되어야 한다.

(let [tm (transient {})]
(doseq [x (range 100)]
(assoc! tm x 0))
(persistent! tm))
;= {0 0, 1 0, 2 0, 3 0, 4 0, 5 0, 6 0, 7 0}

위의 코드에서 tm은 사용되고 있지 않기 때문에 그 값들이 보존되지 않는다.

트랜션트는 이전 데이타를 결코 사용하면 않된다는 제약 조건이 있지만, 존속성 데이타와 똑같은 방식으로 사용되도록 의도되었다. 트랜션트를 사용하는 가장 좋은 방식은 존속성 컬렌션으로 코드를 먼저 만든 다음 transient와 persistent!를 추가하고, 해당 갱신 함수들에 !를 붙이는 것이다.

트랜션트는 한 스레드에서만 사용되어야 한다. future는 새로운 스레드를 만든다.

(let [t (transient {})]
@(future (get t :a)))
;= #<IllegalAccessError java.lang.IllegalAccessError:
;= Transient used by non-owner thread>

트랜잭션은 구성적이지 않다

persistent!는 내부 트랜션트까지 적용되지 않는다.

(persistent! (transient [(transient {})]))
;= [#<TransientArrayMap clojure.lang.PersistentArrayMap$TransientArrayMap@b57b39f>]

트랜션트 컬렉션은 값으로의 의미론적 가치를 갖고 있지 않다. 다음과 같은 비교는 트랜션트에서는 무의미하다.

(= (transient [1 2]) (transient [1 2]))
;= false

트랜션트는 철저하게 로컬 최적화에 특화되어 있는 것이지, 결코 다른 컬렉션으로서 동작하도록 설계된 것은 아니라는 점을 명심해야 한다.

메타데이타

메타데이타는 다른 데이타에 대한 데이타이다. 메타데이타는 여러 가지로 불려지기도 하고 다른 언어에서는 여려 가지 형태를 갖는다.

  • 타입 선언과 접근 수정자는 값, 변수, 함수에 대한 메타데이타이다.
  • 자바 어노테이션은 클래스, 메소드, 메소드 인수 등에 대한 메타데이타이다.

메타데이타는 클로저에서도 같은 역할을 하지만 보다 기능이 더 풍부하다. 메타데이타는 클로저 데이타 구조, 시퀀스, 레코드, 심볼 그리고 레퍼런스 타입에 부착될 수 있는데, 항상 맵 형태를 뛴다.

메타데이타는 몇 가지 편리한 구문을 사용한다.

(def a ^{:created (System/currentTimeMillis)} [1 2 3])
;= #'user/a
(meta a)
;= {:created 1322065198169}

키는 키워드이고 값은 true인 메타데이타는 간단하게 표시한다.

(meta ^:private [1 2 3])
;= {:private true}
(meta ^:private ^:dynamic [1 2 3])
;= {:dynamic true, :private true}

with-meta와 vary-meta 함수로 메타데이타 변경이 가능하다.

(def b (with-meta a (assoc (meta a)
                       :modified (System/currentTimeMillis))))
;= #'user/b
(meta b)
;= {:modified 1322065210115, :created 1322065198169}
(def b (vary-meta a assoc :modified (System/currentTimeMillis)))
;= #'user/b
(meta b)
;= {:modified 1322065229972, :created 1322065198169}

with-meta는 심볼의 메타데이타 자체를 신규로 교체하지만, very-meta는 기존 메타데이타를 갱신 함수를 통해 변경한다.

한가지 명심할 것은 메타데이타는 단지 다른 데이타에 대한 데이타라는 점이다. 그래서 원래의 데이타에는 하등 영향을 주지 않는다.

(= a b)
;= true
a
;= [1 2 3]
b
;= [1 2 3]
(= ^{:a 5} 'any-value
^{:b 5} 'any-value)
;= true

기존 데이타로 새롭게 만들어진 데이타는 기존 데이타의 메타데이터를 그대로 유지한다.

(meta (conj a 500))
;= {:created 1319481540825}

이러한 특성을 잘 이용하는 것은 데이타의 출처를 기록할 수 있다는 것이다. 즉 데이타가 캐쉬에서 왔는지 아니면 데이타베이스에서 왔는지를 기록할 수 있다.

1)
conj은 conjoin
2)
lazy-cons는 권장되지 않음. Goodbye lazy-cons, hello lazy-seq
3)
count도 비슷한 일은 한다. 하지만 아주 드물지만 길이를 유지하는 레이지 시퀀스가 있을 수 있다. 이런 경우 count는 레이지 시퀀스를 실현하지 않고 길이를 리턴한다
4)
predicate는 언어학적으로는 주어에 대한 서술(동사나 형용사)를 의미하지만, 여기서는 수리논리적인 의미로 Boolean-valued function P:X→{true,false}를 의미한다.
lecture/clojure/collections_and_data_structures.txt · Last modified: 2019/02/16 02:51 (external edit)