Skip to content

Latest commit

 

History

History
512 lines (385 loc) · 25.2 KB

File metadata and controls

512 lines (385 loc) · 25.2 KB

Concise Collection Access(간결한 컬렉션 접근)

값을 접근하는 것은 컬렉션, 특히 결합 추상화를 지원하는 것들에 쉽게 수행되는 가장 공통적인 연산이다. 그런 경우, get이나 nth를 계속해서 타이핑하는 것은 매우 피곤한 일이다. 고맙게도, 클로저 컬렉션과 결합 컬렉션에 사용되는 가장 공통된 키 타입은 또한 get이나 nth의 의미론을 가진 함수이다.(관련된 컬렉션의 콘크리트 타입에 적절하게)

Collections are functions(컬렉션은 함수이다.) 매우 간단하게, 클로저 컬렉션은 제공된 키나 인덱스와 결합된 값을 찾는 함수다. 그러므로 아래의 것들은 :

(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같은 두번째 선택적 인자를 받는데, 탐색에 실패하면 선택적인 기본값을 리턴한다. 벡터와 집합은 모두 하나의 값/인덱스만 탐색을 위해 받는다. 기본값은 지원되지 않는다. 벡터 탐색을 위해 제공되는 인덱스는 반드시 벡터의 범위 내부에 있어야만 하는데, 딱 nth와 같다.

([:a :b :c] -1)
;= #<IndexOutOfBoundsException java.lang.IndexOutOfBoundsException>

Collection keys are (often) functions.(컬렉션 키들은 종종 함수들이다.) 유사하게, 키의 가장 공통된 타입—키워드와 심볼— 또한 제공된 컬렉션에서 자신을 탐색하는 함수다. 따라서, 아래의 것들은

(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

함수 위치의 값이 반드시 함수가 되야 하므로, 수치적 인덱스는 사용될 수 없다. 결국, 벡터 탐색은 이런 접근법을 사용해서 수행될 수 없다.

Idiomatic Usage(관용적 용법)

훌륭하게도, 컬렉션에서 값을 접근하는 덜 자세한 방식이 있다. 명백하게 좋지만, 일부 추가적인 안내 없이는, 각각의 변종을 언제 어떻게 써야하는지 분명하지가 않을 것이다. 언제 컬렉션이 키워드나 심볼에 대하여 탐색 함수처럼 사용되야 하는가?

이것은 위험스럽게 취향의 영역 안에 속해 있다. 그러나 일반적으로, 우리는 탐색이 될 키워드나 심볼을 함수처럼 사용하는 것을 권장한다. 이 idiom의 가장 즉각적인 이점은 널 포인터 예외가 보통 회피된다는 것인데, 키워드와 심볼은 탐색 함수로 가장 빈번하게 사용되는 리터럴이기 때문이다. 생각해보자.

(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>

추가적으로, (coll :foo) 폼은 coll, 즉 컬렉션, 또한 함수라고 추정한다. 그것은 대부분의 클로저 데이터 구조체에 참이지만, (예를들어) 리스트에는 참이 아니며, 클로저의 컬렉션 추상화에 참여하는 다른 타입들은 필연적인 참도 아니고 또한 함수도 아니다. 이 때문에 여러분이 그 리터럴 키워드 :foo가 항상 함수이고 결코 nil이 아님을 확신할 수 있는 한, (:foo coll)가 더 바람직하다. 반면에 coll에 의해 참조되는 값은 조건을 충족하지 못할 것이다.

물론, 만약 컬렉션이 키워드나 심볼이 아닌 다른 키를 가지면, 반드시 탐색 함수로 컬렉션이나 get이나 nth를 사용해야만 한다.

Collections and Keys and Higher-Order Functions(컬렉션과 키와 고차원 함수)

키워드, 심볼, 그리고 많은 컬렉션이 함수이기 때문에, 그들을 고차 함수에 대한 입력으로 사용하는 것은 일반적이며 굉장히 편리하다. 우리가 우리 고객들 전부의 이름을 원한다고 해보자. 어떤 함수를 정의할 필요도 없고, get의 사용을 명시할 필요도 없다.

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

some은 제공된 술어로부터 논리적 참 값이 리턴되는 시퀀스의 첫번째 값을 찾는다. some을 집합과 함께 사용하는 것은 일반적인 패턴이다.

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

이것이 some을 조건절에서 컬렉션 탐색 결과를 사용하는 매우 간결한 방식으로 만든다. 더 일반적인 연산은 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를 보완하는데, 문자 그대로이다. 주어진 함수의 complement와 함께 주어진 컬렉션을 거르는 것에 의해 구현되는데, (filter (complement f) collection).

(다시) nil을 인지하기
집합을 사용해서 어떤 값이 주어진 컬렉션에 소유되는지 아닌지 테스트하는 것은 간단한데, 문제의 값이 nil이나 fasle인 때, 결과가 우리의 기대대로 할당되지 않을 수 있는데, 그 두가지 값이 모두 논리적인 거짓이기 때문임을 망각하기가 쉽다.

(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를 가지지 않을 것인지 확신할 수 없는 때라면, get을 통하여 contains?를 사용하거나 직접 contains?를 호출하는 것이 더 낫다. :


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

Data Structure Types(데이터 구조 타입들)

클로저는 상당수의 콘크리트 타입을 제공하는데, 각 타입은 다양한 추상화(추상물들)를 적절하게 충족한다. 우리는 이미 이들 콘크리트 구현체들과 꽤 많이 작업해 오고 있다—진정, 우리가 희망했던 대로, 콘크리트 구현체들의 동작과 의미론은 대부분 그 구현체들이 참여하는 추상화(또는 추상화의 부분들)에 의해 정의된다.

여기서, 우리는 각각의 콘크리트 데이터 구조 타입을 나누어서 일부 구현체의 세부사항들을 빠르게 훓고 지나갈 것인데, 대부분 그들의 생성물과 함께 보아야 한다.

Lists

리스트는 클로저에서 가장 단순한 컬렉션 타입이다. 그들의 기본적이고 가장 일반적인 목적은 클로저 코드에서 호출에 대한 표현을 수행하는 것인데, 7페이지의 식, 연산, 문법과 우선 순위에서 설명했다. 그러므로, 여러분은 프로그램의 런타임시에 여러분이 항상 하게 될 것 보다 여러분의 소스 파일에서 리터럴만큼이나 많이 그들을 사용하게 될 것이다.

클로저 리스트는 하나씩 연결되지만, 그들의 머리에서 효율적으로 접근되거나 “수정”되는데, conj를 사용해서 새로운 머리 값을 집어넣거나, pop 또는 차례열 연산자 rest를 써서 이전의 머리 값이 없는 부분 리스트에 대한 참조를 얻는다. 리스트가 연결된 리스트이기 때문에, 효율적인 임의 접근을 지원하지 못 한다. 따라서, 리스트에 대한 nth는 선형 시간으로 동작되고(벡터, 배열 등과 사용될 때 즉시인 것과 대조된다), get은 리스트를 전혀 지원하지 않는데 그렇게 하면 get의 선형 이하 효율성의 목적에 부합되지 않기 떄문이다.

리스트가 그들 자신의 차례열이라는 것은 아무런 가치도 없다. 따라서, 리스트의 seq는 항상 그 리스트를 리턴하고 리스트에 대한 별개의 차례열 모습을 리턴하지는 않는다.

우리는 전에 리스트 리터럴을 본 적이 있다.

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

만약 우리가 이 리스트에 쿼팅을 하지 않으면, 값 1의 호출로 평가될 것이고, 이는 실패하게 된다. 쿼팅의 부수 효과는 리스트 리터럴 안의 식들 또한 평가되지 않는 것이다.

'(1 2 (+ 1 2))
;= (1 2 (+ 1 2))

대부분의 사람은 단순히 그런 경우 벡터 리터럴을 사용하는데, 백터 리터럴 안에서는 멤버 식들이 항상 평가 될 것이다. 그러나, 다른 데이터 구조로는 안되고, 진짜로 리스트가 필요한 경우가 있다. 그런 때에는, 리스트에 도달한다.

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

list는 상당수의 값들을 인자로 받는데, 각 값은 리턴된 리스트의 요소가 될 것이다.

마지막으로, 여러분은 list? 술어를 사용해서 값이 정확하게 리스트인지 테스트할 수 있다.

Vectors

벡터는 순차적 데이터 구조로 효율적인 임의 접근 탐색과 변경을 지원하는데 java.util.ArrayList, 파이썬의 리스트 그리고 루비의 배열을 썼던 프로그래머들의 기대에 맞을 것이다. 벡터 또한 특히 다목적이고, 우리가 이미 본 것 처럼 결합, 인덱스 그리고 스택 추상화에 참여하고 있다.

지금까지 친숙했던 벡터 리터럴을 제외하고, 벡터는 vector와 vec를 사용하여 만들 수 있다.

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

vector는 list와 닮아 있는 반면, vec는 오로지 하나의 순차적 인자를 기대하는데 그 인자의 전체 내용이 새 벡터 생성에 사용될 것이다. 이는 여러분이 배열, 리스트, 차례열 또는 다른 seqable 값에서 어떤 데이터를 가지고 있을 때 유용한데, 그 데이터의 추가 조작이 벡터의 기능으로부터 이익을 얻기 위해 필요하다.

vector?는 list?와 예측할 수 있게 유사한데, 값이 벡터인지 아닌지 테스트하는데 사용된다.

Vectors as tuples(튜플로써의 벡터)

튜플은 벡터의 가장 일반적인 사용 예 중 하나이다. 어떤 때는 몇개의 값들이 가능한한 적은 동작으로 함께 집결되야 하는데—마치 함수로부터 다수의 값이 리턴될 때 처럼—그 값들이 벡터에 들어갈 수 있다.

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

① 더 간결하게 하고 싶다면, (juxt quot rem)이 이것과 같은 함수를 리턴한다.

이것은 클로저의 넓게 퍼진 인자분해 메커니즘(28페이지의 인자분해(let, 파트2)를 보라)과 멋지게 쌍을 이루는데, 리턴된 값에서 내용물을 쉽게 풀어 해치게 해준다.

(let [[q r] (euclidian-division 53 7)]
  (str "53/7 = " q " * 7 + " r))
;= "53/7 = 7 * 7 + 4"

튜플들이 매력적이고 쉬운 만큼, 여러분은 튜플이 (an expeditive mean to an end)임을 잊지 말아야 한다. 튜플은 공공 API의 일부로 노출되기 보다 여러분의 라이브러리나 모듈 내부에 숨겨두는 것이 최선이다. 이에 대한 근거는 아래의 두 가지다.

  • 튜플은 self-documenting이 아니다. 여러분은 지속적으로 각 인덱스의 개별적인 역할을 재호출해야만 한다.

  • 튜플은 유연하지 않다. 여러분은 특정한 리턴 값에 적절하지 않은 중간 슬롯에도 값을 제공해야만 하며 튜플의 꼬리 뒤쪽에 추가하는 것 이외 다른 방식으로 튜플을 확장할 수 없다.

맵은 이들 두 개의 제한으로 고통받지 않는다. 따라서, 공공 API의 일부인 함수와 간단치 않은 리턴 값에 대하여는, 맵이 더 잘 들어 맞는다.

물론, 어떤 규칙과 함께 하는 것처럼, 이것은 예외가 있다. 튜플의 목적이 명백하게 근접한 영역을 위한 것인 경우—좌표, 그래프에서 지향된 끝점 등—튜플로 벡터를 사용하는 것은 완전 적절하다.

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

위의 뒤쪽 사례에서, 우리가 두 가지 다른 벡터의 용법을 사용한 것을 볼 수 있다. 바깥쪽 벡터는 그냥 더 나은 리스트이고, 반면에 안쪽 벡터는 세 문자로 된 공항표식의 튜플인 [from to]로 사용된다.

Sets(집합)

우리가 결합과 집합 추상화를 논의할 때 접해 본 적이 없는 콘크리트 데이터 구조 구현체로써의 집합에 관해 거의 얘기해 본 적이 없다. 클로저의 다른 데이터 구조 타입들처럼, 집합은 우리가 이미 본적이 있는 리터럴 표기법을 가진다.

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

① 정의에 의한 집합은 중복 값을 포함할 수 없기 때문에, 중복값을 포함하는 리터럴은 거부된다.

그리고 hash-set 함수는 여러개의 인자를 받아서 정렬되지 않은 집합을 만들 때 사용한다.:

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

마지막으로, 여러분은 set함수를 사용해서 어떤 컬렉션에 있는 값들로부터도 집합을 만들 수 있다.

(set [1 6 1 8 3 7 7])
;= #{1 3 6 7 8}

이것은 실제로 seqable한 어떤 것과도 작동하며,

(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

정렬된 집합 변종도 이용가능한데, 106페이지의 정렬(Sorted)에서 보았다.

Maps(맵)

지금까지 친숙한 맵 리터럴을 제외하고 :

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

① 집합에서 값이 그랬듯이, 맵에서는 키가 유니크해야 한다. 이 요구사항에 저촉되는 리터럴은 거부된다.

정렬되지 않은 맵은 hash-map을 써서 만들 수 있는데, hash-map은 다수의 키/값 쌍을 인자로 받는다. hash-map은 여러분이 벡터 튜플에 묶여 있지 않은 키/값 쌍들의 컬렉션을 가지고 있을 때 apply와 함께가장 빈번하게 사용된다.

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

정렬된 맵 변종도 이용가능한데, 106페이지 “sorted”에서 보았다.

keys and vals.

맵에 특화되었지만, keys, vals 함수는 원본 맵으로부터 키나 값의 차례열을 리턴하는 간단한 도구이다.:

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

이들 함수는 엔트리들로 된 차례열을 얻기 위해 맵에 seq를 사용하기 위한 토대가 되는 단축 도구로, 각 엔트리로부터 key와 val을 얻는다.

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

Maps as ad-hoc structs(애드혹 구조체로써의 맵)

맵 값이 어떤 타입도 될 수 있기 때문에, 단순하고 유연한 모델로써 빈번하게 사용되는데, 각 필드(슬롯이라고도 부른다)를 식별하기 위한 키로써 키워드를 가장 자주 함께 사용한다.

(def playlist
  [{:title "Elephant", :artist "The White Stripes", :year 2003}
   {:title "Helioself", :artist "Papas Fritas", :year 1997}
   {:title "Stories from the City, Stories from the Sea",
    :artist "PJ Harvey", :year 2000}
   {:title "Buildings and Grounds", :artist "Papas Fritas", :year 2000}
   {:title "Zen Rodeo", :artist "Mardi Gras BB", :year 2002}])

단순한 맵으로 시작하는 것이 클로저 데이터 모델링에서 매우 일반적이다. 특히 엔트리를 구성하게 될 슬롯이 어떤 것인지 불분명할 때, 맵은 엄격한 데이터 모델을 미리 정의할 필요없이 바로 작업을 하게 해준다.

일단 여러분이 여러분의 모델을 위해 맵(그리고 클로저의 추상에 참여하는 다른 데이터 구조)를 사용하면, 맵의 모든 기능은 여러분이 모델로 하려는 어떤 작업에도 흘러들어 간다. 예를들어, 키워드를 키로사용했다고 가정하면, 데이터 집계 쿼리는 간소해진다.:

(map :title playlist)
;= ("Elephant" "Helioself" "Stories from the City, Stories from the Sea"
;=  "Buildings and Grounds" "Zen Rodeo")

유사하게, 결합 인자분해 같은 일은, 32페이지의 맵 인자분해에 소개했는데, 개별적인 맵 구조체에 대한 연산을 단순화하는데 도움이 되어서, 보다 상세한 (:slot data) 접근들을 제거할 수 있다.

(defn summarize [{:keys [title artist year]}]
  (str title " / " artist " / " year))

클로저는 깔끔한 업그레이드 경로—여러분이 필요하다면—맵 기반 구조체를 사용하는 프로토타이핑 부터 더 성숙한 모델까지 제공하여 여러분을 미숙한 오버 아키텍팅으로부터 구하려고 노력한다. 따라서, 맵으로 모델링하는 것은 막다른 것이 아니다. 어떤 특정한 구현체(the latter being something you really need to go out of your way to do) 대신 클로저의 컬렉션 추상으로 프로그래밍하는 한, 여러분은 깔끔하게 맵 기반 모델을 특화된 타입—항상 결합 타입을 생산하는 defrecord에 의해 정의된 것처럼—으로 교환할 수 있을 것이다.

defrecord는 270페이지의 여러분 자신의 타입을 정의하기에서 세부적으로 논의할 것이고, 277페이지의 “맵이나 레코드를 사용하는 때”에서 defrecord와 맵을 비교한다.

Other usages of maps(맵의 다른 용법들)

맵은 또한 요약이나 인덱스나 번역 테이블로도 사용된다. 여기서는 데이터베이스 인덱스와 뷰를 생각해본다.

예를 들어, group-by는 컬렉션을 키 함수에 따라 파티셔닝하는데 매우 유용하다.

(group-by #(rem % 3) (range 10))
;= {0 [0 3 6 9], 1 [1 4 7], 2 [2 5 8]}

여기서 우리는 제공된 함수에 의해 정의된 키로 그룹화된 숫자들을 본다. 우리의 플레이리스트 데이터와 함께, 우리는 아티스트로 앨범의 인덱스를 쉽게 만들 수 있다.

(group-by :artist playlist)
;= {"Papas Fritas" [{:title "Helioself", :artist "Papas Fritas", :year 1997}
;=                  {:title "Buildings and Grounds", :artist "Papas Fritas"}]
;=  ...}

두 개 컬럼에 대한 인덱스는 (group-by (juxt :col1 :col2) data) 만큼이나 쉽다.

때때로 여러분은 아이템들의 벡터를 리턴하기 보다 주어진 키에 대한 아이템의 요약을 계산하고 싶을 때가 있다. 여러분은 group-by를 사용해서 각 값을 처리하여 그것을 요약할 수 있다.

(into {} (for [[k v] (group-by key-fn coll)]
           [k (summarize v)]))

key-fn과 summarize는 여러분의 실제 함수를 위한 예약공간이다. 그러나, 데이터 베이스 쿼리로부터의 커다란 결과 집합을 다룰 때처럼, 여러분의 컬렉션이 특히 크다면 이것이 성가시게 될 수 있다. 이 경우에, 여러분은 group-by와 reduce를 이용한 여러분만의 혼합에 의존해야 한다. reduce-by는 데이터에 대한 모든 종류의 요약을 계산하는데 도움을 줄것인데, SQL의 SELECT … GROUP BY … 쿼리와 다르지 않다.

(defn reduce-by
  [key-fn f init coll]
   (reduce (fn [summaries x]
            (let [k (key-fn x)]
              (assoc summaries k (f (summaries k init) x))))
    {} coll))
x, xs, and other not-so-cryptic names(x, xs, 그리고 다른 not-so-cryptic 이름들)

클로저 초심자는 x와 xs 같이 혼란을 주는 매우 간단한 이름의 사용을 자주 발견한다. 각 문자가 의미하는 것은 라이브러리 코딩 표준 스타일 가이드(http://dev.clojure.org/display/design/Library+Coding+Standards)에 의해 부분적으로 성문화 된다. 핵심은, 여러분이 이름 x를 사용하면, 여러분은 여러분의 코드가 x의 타입에 generic하고 oblivious하다고 말하는 것이며, (there’s no point in calling it invoice) 그것의 호출은 핵심이 아니다. 비슷하게, x값들의 컬렉션이나 차례열은 빈번하게 xs라고 이름을 붙이는데, 이런 것 등이 있다. 여러분의 코드가 더 generic해질 수록, 여러분이 사용하게 될 이름은 덜 특화되게 된다.

우리가 ACME 기업에 대한 구매 주문 목록을 가지고 있다고 가정해서, “구조체”로써 평이한 맵을 사용해서 나타내면

(def orders
  [{:product "Clock", :customer "Wile Coyote", :qty 6, :total 300}
   {:product "Dynamite", :customer "Wile Coyote", :qty 20, :total 5000}
   {:product "Shotgun", :customer "Elmer Fudd", :qty 2, :total 800}
   {:product "Shells", :customer "Elmer Fudd", :qty 4, :total 100}
   {:product "Hole", :customer "Wile Coyote", :qty 1, :total 1000}
   {:product "Anvil", :customer "Elmer Fudd", :qty 2, :total 300}
   {:product "Anvil", :customer "Wile Coyote", :qty 6, :total 900}])

reduce-by로, 우리는 고객에 의한 주문 총계를 쉽게 계산할 수 있다.

(reduce-by :customer #(+ %1 (:total %2)) 0 orders)
;= {"Elmer Fudd" 1200, "Wile Coyote" 7200}

마찬가지로, 각 상품별 고객들을 얻을 수도 있다.

(reduce-by :product #(conj %1 (:customer %2)) #{} orders)
;= {"Anvil" #{"Wile Coyote" "Elmer Fudd"},
;=  "Hole" #{"Wile Coyote"},
;=  "Shells" #{"Elmer Fudd"},
;=  "Shotgun" #{"Elmer Fudd"},
;=  "Dynamite" #{"Wile Coyote"},
;=  "Clock" #{"Wile Coyote"}}

만약 여러분이 2단계 분석을 원한다면, 고객에 의해 모든 것을 정렬하고, 그 다음에 상품에 의해 정렬하고 싶다고 가정해보면?

여러분은 간단히 키로써 그 두개의 값의 벡터를 리턴할 필요가 있다. 여기 그런 함수를 작성하는 몇 가지 방법이 있다.

(fn [order]
  [(:customer order) (:product order)])
#(vector (:customer %) (:product %))
(fn [{:keys [customer product]}]
  [customer product])
(juxt :customer :product)

우리는 가장 깔끔하고 간결한 것을 선호할 것이다.:

(reduce-by (juxt :customer :product)
  #(+ %1 (:total %2)) 0 orders)
;= {["Wile Coyote" "Anvil"] 900,
;=  ["Elmer Fudd" "Anvil"] 300,
;=  ["Wile Coyote" "Hole"] 1000,
;=  ["Elmer Fudd" "Shells"] 100,
;=  ["Elmer Fudd" "Shotgun"] 800,
;=  ["Wile Coyote" "Dynamite"] 5000,
;=  ["Wile Coyote" "Clock"] 300}

우리가 기대했던 것이 아니다—여기에는 맵의 맵이 없다. 이 문제는 맵이 얕다고 가정하는 reduce-by에 있다. 여러분은 중첩된 맵을 위한 버전을 만들어서 reduce-by를 고치거나, 아니면 여러분의 결과 맵에게 메시지를 보낼 수 있다.

reduce-by가 중첩된 맵과 동작하게 만드는 것은 assoc과 묵시적 get에 대한 호출을 assoc-in과 get-in으로 교체하는 것만큼 쉽다.

(defn reduce-by-in
  [keys-fn f init coll]
  (reduce (fn [summaries x]
            (let [ks (keys-fn x)]
              (assoc-in summaries ks
                (f (get-in summaries ks init) x))))
    {} coll))

기대된 대로, 우리는 이제 두 단계 분석을 얻을 수 있다.

(reduce-by-in (juxt :customer :product)
  #(+ %1 (:total %2)) 0 orders)
;= {"Elmer Fudd" {"Anvil" 300,
;=                "Shells" 100,
;=                "Shotgun" 800},
;=  "Wile Coyote" {"Anvil" 900,
;=                 "Hole" 1000,
;=                 "Dynamite" 5000,
;=                 "Clock" 300}}

두번째 옵션은 우리의 이전 결과 데이터를 변경하는 것이다.

(def flat-breakup
  {["Wile Coyote" "Anvil"] 900,
   ["Elmer Fudd" "Anvil"] 300,
   ["Wile Coyote" "Hole"] 1000,
   ["Elmer Fudd" "Shells"] 100,
   ["Elmer Fudd" "Shotgun"] 800,
   ["Wile Coyote" "Dynamite"] 5000,
   ["Wile Coyote" "Clock"] 300})

…기대된 맵들의 맵으로 말이다. 그러기 위해, 우리는 assoc-in을 또 사용하고 있다.

(reduce #(apply assoc-in %1 %2) {} flat-breakup)
;= {"Elmer Fudd" {"Shells" 100,
;=                "Anvil" 300,
;=                "Shotgun" 800},
;=  "Wile Coyote" {"Hole" 1000,
;=                 "Dynamite" 5000,
;=                 "Clock" 300,
;=                 "Anvil" 900}}

flat-breakup 맵에 의하여 seq에 제공되는 각각의 값은 [[“Wile Coyote” “Anvil”] 900]같은 맵의 엔트리이다. 따라서, 우리의 reduction 함수가 이들 맵 엔트리 각각에 apply를 적용할 때, assoc-in에 대한 호출 결과적으로—예를 들면, (assoc-in {} [“Wile Coyote” “Anvil”] 900)—간편하게 각 엔트리의 데이터를 사용해서 결과 맵의 구조와 그 맵의 가장 깊은 값들을 정의한다.