- Learn about Wiki
- Lectures
- Study
- Tips
클로저는 추상 데이타 구조인 시퀀스 컬렉션과 맵 컬렉션을 제공한다. 클로저의 많은 함수들은 구체적인 데이타 구조보다는 추상 데이타 구조를 파라미터로 받고 리턴하는데, 이로 인해 구체 데이타 구조로 인해 발생할 수 있는 우연적 복잡성이 제거되어, 함수들은 레고 블럭처럼 구성가능하게 된다.
추상 데이타 구조에 대한 접근하는 방법은 매우 일반적이다.
(def v [42 "foo" 99.2 [5 12]]) ;=> #'user/v (first v) ;=> 42 (second v) ;=> "foo" (last v) ;=> [5 12] (nth v 2) ;=> 99.2 ;; 1 (v 2) ;=> 99.2 ;; 2 (.get v 2) ;=> 99.2 ;; 3
이러한 방법은 컬렉션의 요소 하나를 얻을 때는 좋다. 하지만 여러 개의 요소에 접근할 때는 번거로워진다.
(+ (first v) (v 2)) ;=> (+ 42 99.2) => 141.2 (+ (first v) (first (last v))) ;=> (+ 42 (first [5 12])) => (+ 42 5) => 47
인수분해는 이러한 문제를 해결해 주는데, 컬렉션을 원하는 대로 요소별로 가져와서 let 바인딩 벡터의 로컬 심볼에 바인딩한다. 인수분해는 let 문구뿐 만 아니라, let 바인딩을 내부적으로 사용하는 fn, defn, loop 등에서도 사용될 수 있다.
인수분해는 그 대상에 따라 시퀀스 인수분해와 맵 인수분해가 있다.
시퀀스 인수분해의 대상은 모든 시퀀스 컬렉션이다.
다음은 간단한 시퀀스 인수분해이다.
(def v [42 "foo" 99.2 [5 12]]) ;=> #'user/v (let [[x y z] v] (+ x z)) ;=> 141.2
let 바인딩 벡터는 이름-값 쌍의 나열이다. 위 코드에서는 이름이 [x y z]이고, 값은 v인 쌍 하나만 있다. 값 v는 하나의 심볼이 아닌 [x y z]라는 심볼 시퀀스로 바인딩되기 위해 인수분해 되어야 한다. 물론 v가 시퀀스 인수분해되기 위해서는 v 자체가 시퀀스여야 한다. 시퀀스 인수분해는 자리별로 바인딩된다. 즉 v의 첫 요소는 x, 둘째 요소는 y, 세째 요소는 z로 바인딩된다.
사실 위의 코드는 아래 코드와 같은 일을 하는 것이다.
(let [x (nth v 0) y (nth v 1) z (nth v 2)] (+ x z)) ;=> 141.2
다음은 인수분해가 내부 벡터에 적용되는 예이다.
(let [[x _ _ [y z]] v] (+ x y z)) ;=> 59
인수분해는 자리별로 서로 매칭되어 분해되서 바인딩되는 것이다.
위의 기본 시퀀스 인수분해는 다음과 같이 자리별 매칭이 된다.
[x y z] [42 "foo" 99.2 [5 12]]
위의 내부 시퀀스 인수분해는 다음과 같이 자리별 매칭이 된다.
[x _ _ [y z ]] [42 "foo" 99.2 [5 12]]
&를 사용하면 나머지 요소들을 시퀀스로 인수분해할 수 있다.
(let [[x & rest] v] rest) ;=> ("foo" 99.2 [5 12])
이것은 전형적인 시퀀스 인수분해이다. 이런 인수분해는 특히 loop등 재귀 호출에서 많이 사용된다. 한가지 주의할 점은 rest가 벡터가 아니라 시퀀스라는 점이다.
때로는 원래의 값을 그대로 유지하고 싶을 수도 있다. 그럴 때는 :as 키워드를 사용한다.
(let [[x _ z :as org] v] (conj org (+ x z))) ;=> [42 "foo" 99.2 [5 12] 141.2]
이것이 유용할 때는 v가 함수일 경우이다. 함수의 결과값을 인수분해하기 했지만 결과값 전체를 지시하는 심볼이 없어 함수를 다시 호출하지 않기 위해서이다. 다음 코드를 보자.
(defn f [] [1 2 3]) ;=> #'user/f (let [[x y] (f)] (conj (f) (+ x y))) ;; f 함수가 2번 호출된다. ;=> [1 2 3 3] (let [[x y :as all] (f)] (conj all (+ x y))) ;; f 함수의 결과값을 심볼 all로 받아 사용한다. ;=> [1 2 3 3]
다음은 나머지 인수분해와 원본 인수분해를 같이 사용하는 예이다.
(let [[a b c & more :as all] (range 10)] (println "a b c are: " a b c) (println "more is: " more) (println "all is: " all)) ;>> a b c are: 0 1 2 ;>> more is: (3 4 5 6 7 8 9) ;>> all is: (0 1 2 3 4 5 6 7 8 9) ;=> nil
맵 인수분해의 대상은 다음과 같다.
다음은 기본적인 맵 인수분해이다.
(def m {:a 5 :b 6 :c [7 8 9] :d {:e 10 :f 11} "foo" 88 42 false}) ;=> #'user/m (let [{a :a b :b} m] (+ a b)) ;=> 11
위 코드에서 let 바인딩 벡터는 인수분해를 위해 맵을 사용하여, m의 :a 값인 5를 a에 m의 :b 값인 6을 b에 바인딩한다.
맵은 키-값 쌍을 요소로 하기 때문에 다음과 같이 키에 따른 분해가 된다고 생각할 수 있다.
{a :a b :b} {:a 5 :b 6}
맵의 키는 키워드외에 다른 것이 올 수도 있기 때문에 다음 코드도 가능하다.
(let [{f "foo"} m] (+ f 12)) ;=> 100
(let [{v 42} m] (if v 1 0)) ;=> 0
맵 인수분해에서 벡터나 스트링의 인덱스는 키로 사용될 수 있다. 다음은 벡터를 맵 인수분해하는 예이다.
(let [{x 3 y 8} [12 0 0 -18 44 6 0 0 1]] (+ x y)) ;=> -17
벡터를 맵 인수분해하는 장점은 특정 자리만을 골라서 인수분해할 수 있다는 점이다.
벡터는 위치 인덱스를 키로 하는 맵이다.
다음은 내부 맵에 대한 인수분해이다.
(let [{{e :e} :d} m] (* 2 e)) ;=> 20
:d에 의해 m의 내부 맵 {:e 10 :f 11}이 선택되고, 다시 :e에 의해 10이 선택된다.
맵 인수분해와 시퀀스 인수분해가 같이 사용되면 우아한 코드가 된다.
(let [{[x _ y] :c} m] (+ x y)) ;=> 16
(def map-in-vector ["James" {:birthday (java.util.Date. 73 1 6)}]) ;=> #'user/map-in-vector (let [[name {bd :birthday}] map-in-vector] (str name " was born on " bd)) ;=> "James was born on Thu Feb 06 00:00:00 EST 1973"
시퀀스 인수분해에서처럼 :as를 사용하면 인수분해되는 맵 자체를 바인딩할 수 있다.
(let [{r1 :x r2 :y :as randoms} (zipmap [:x :y :z] (repeatedly (partial rand-int 10)))] (assoc randoms :sum (+ r1 r2))) ;=> {:sum 17, :z 3, :y 8, :x 9}
인수분해 문구에서 피인수분해 맵에는 없는 키를 사용했을 때, 기본맵을 제공하여 해당 키의 값을 설정할 수 있다.
(let [{k :unknown x :a :or {k 50}} m] (+ k x)) ;=> 55
아래 코드는 같은 결과를 낸다.
(let [{k :unknown x :a} m k (or k 50)] (+ k x)) ;=> 55
하지만 :or는 피인수분해의 해당 키 값이 false이거나 nil일 때도 동작한다.
(let [{opt1 :option} {:option false} opt1 (or opt1 true) {opt2 :option :or {opt2 true}} {:option false}] {:opt1 opt1 :opt2 opt2}) ;=> {:opt1 true, :opt2 false}
맵의 키는 그 자체로 데이타의 성격을 드러내는 경우, 맵 인수분해 이후에도 그 키의 이름을 그대로 사용하는 것이 좋은데, 다음과 같이 같은 이름들이 반복되게 된다.
(def kildong {:name "KilDong" :age 24 :location "west"}) ;=> #'user/kildong (let [{name :name age :age location :location} kildong] (format "%s is %s years old and lives in %s." name age location)) ;=> "KilDong is 24 old years and lives in west."
이런 반복을 하지 않기 위해 :keys를 사용하여 피인수분해 맵의 각 키의 이름으로 바인딩한다.
(def kildong {:name "KilDong" :age 24 :location "west"}) ;=> #'user/kildong (let [{:keys [name age location]} kildong] (format "%s is %s years old and lives in %s." name age location)) ;=> "KilDong is 24 old years and lives in west."
피인수분해 맵이 키로 스트링이나 심볼을 사용하는 경우는 :strs과 :syms를 사용한다.
(def kildong {"name" "KilDong" "age" 24 "location" "west"}) ;=> #'user/kildong (let [{:strs [name age location]} kildong] (format "%s is %s years old and lives in %s." name age location)) ;=> "KilDong is 24 old years and lives in west."
(def kildong {'name "KilDong" 'age 24 'location "west"}) ;=> #'user/kildong (let [{:syms [name age location]} kildong] (format "%s is %s years old and lives in %s." name age location)) ;=> "KilDong is 24 old years and lives in west."
시퀀스 인수분해서는 &를 사용하여 나머지 요소를 시퀀스로 바인딩할 수 있었다. 키-값 쌍이 튜플로 있는 벡터에 대해서는 튜플들을 맵으로 인수분해할 수 있다.
(def movie ["Les Miserables" 2012 :director "Tom Hooper" :rating 8.0]) ;= #'user/movie (let [[movie-name year & rest] movie {:keys [director rating]} (apply hash-map rest)] (format "%s is made by %s in %s, rating %.1f" movie-name year director rating))
이 코드에서는 시퀀스 인수분해에서 받은 rest를 맵 인수분해하기 위해 hash-map를 적용하고 있다. 이것은 다음과 같이 간단하게 처리될 수 있다.
(let [[movie-name year & {:keys [director rating]}] movie] (format "%s is made by %s in %s, rating %s" movie-name year director rating))
rest 자리에 직접 맵 인수분해 문구를 바로 적용할 수 있다.
위에서 시퀀스를 맵 인수분해 할 수 있음을 보았다. 그것은 시퀀스도 맵 인수분해가 요구하는 get 메소드를 지원하기 때문이다. 하지만 반대로 맵을 시퀀스 인수분해 할 수는 없는데, 맵은 시퀀스 인수분해가 요구하는 nth를 지원하지 않기 때문이다.
특히 주의할 점은 집합은 값(Value)를 키(Key)로 하는 맵이기 때문에 시퀀스 인수분해가 되지 않는다.
(let [[a & r] #{1 2 3}] a) ; UnsupportedOperationException nth not supported on this type: PersistentHashSet...