리듀서
리듀서 (Reducers)
리듀서(reducers)는 표준 Clojure 컬렉션을 조작하는 데 시퀀스를 사용하는 것에 대한 대안적 접근법을 제공해요. 시퀀스 함수는 전형적으로 지연(lazily)되고, 순서대로, 중간 결과를 만들며, 단일 스레드에서 적용돼요. 하지만 많은 시퀀스 함수(예: map, filter)는 개념적으로는 병렬로 적용될 수 있어서, 머신의 코어가 늘어날수록 자동으로 빨라지는 코드를 만들 수 있어요. 리듀서의 근거(rationale)에 대한 자세한 내용은 원래 블로그 게시물을 참고하세요.
reducer 는 축약 가능한 컬렉션(reduce 가능한(reducible) 컬렉션, 즉 자기 자신을 축약하는 방법을 아는 컬렉션)과 축약 함수(reduction 중에 무엇을 할지에 대한 "레시피")의 결합이에요. 표준 시퀀스 연산은 연산을 수행하지 않고 축약 함수만 변환하는 새 버전으로 대체돼요. 연산의 실행은 최종 축약이 수행될 때까지 지연돼요. 이는 시퀀스에서 보이는 중간 결과와 지연 평가를 제거해요.
또한 일부 컬렉션(영속 벡터와 맵)은 foldable 이에요. reducer에 대한 fold 연산은 축약을 병렬로 실행하는데, 다음과 같이 해요:
. 지정된 세분성(granularity, 기본 512개 요소)으로 reducible 컬렉션을 분할하고 . 각 분할에 reduce를 적용하고 . Java의 fork/join 프레임워크를 사용해 각 분할을 재귀적으로 결합해요.
컬렉션이 폴딩을 지원하지 않으면, 병렬이 아닌 reduce로 대신 폴백해요.
본문
reduce와 fold
clojure.core.reducers 네임스페이스(여기서는 r으로 별칭)는 대안적인 r/reduce 함수를 제공해요.
(r/reduce f coll) +
(r/reduce f init coll)
reducers 버전은 다음 점에서 달라요:
- 맵 컬렉션은 reduce-kv로 축약돼요
- init이 제공되지 않으면, 식별 값(identity value)을 만들기 위해 f가 인자 없이 호출돼요 ** 참고: 식별 값을 제공하기 위해 f가 여러 번 호출될 수 있어요
일반적으로 대부분의 사용자는 r/reduce를 직접 호출하지 않고, 병렬 reduce와 combine을 구현하는 r/fold를 선호해요. 하지만 중간 결과가 더 적은 eager reduce를 실행하는 데는 유용할 수 있어요.
(r/fold reducef coll) +
(r/fold combinef reducef coll) +
(r/fold n combinef reducef coll)
r/fold는 reducible 컬렉션을 받아 대략 n(기본 512)개 요소의 그룹으로 분할해요. 각 그룹은 reducef 함수로 축약돼요. reducef 함수는 각 분할에서 식별 값을 만들기 위해 인자 없이 호출될 거예요. 그 축약들의 결과는 combinef(기본은 reducef) 함수로 축약돼요. combinef가 인자 없이 호출되면 그 식별 요소를 만들어야 해요 — 이것은 여러 번 호출돼요. 연산은 병렬로 수행될 수 있어요. 결과는 순서를 보존해요.
다음 함수들(시퀀스 버전과 유사)은 reducible 또는 foldable 컬렉션에서 reducer를 만들어요: r/map r/mapcat r/filter r/remove r/flatten r/take-while r/take 및 r/drop. 이 함수들 중 어느 것도 소스 컬렉션을 실제로 변환하지 않아요. 누적 결과를 만들려면 r/reduce나 r/fold를 사용해야 해요. 출력 컬렉션을 만들려면 컬렉션 타입을 고르는 clojure.core/into를, reducible·foldable·seqable·counted인 컬렉션을 만들려면 제공되는 r/foldcat를 사용해요.
리듀서 사용하기 (Using Reducers)
+로 합하기 위해 fold 사용:
(require '[clojure.core.reducers :as r])
(r/fold + (r/filter even? (r/map inc [1 1 1 2])))
;=> 6
최종 컬렉션을 만들기 위해 into 사용:
(into [] (r/filter even? (r/map inc (range 100000))))
또는 r/foldcat:
(r/foldcat (r/filter even? (r/map inc (range 100000))))
fold와 함께 축약 함수와 결합 함수를 지정:
(defn count-words
([] {})
([freqs word]
(assoc freqs word (inc (get freqs word 0)))))
(defn merge-counts
([] {})
([& m] (apply merge-with + m)))
(defn word-frequency [text]
(r/fold merge-counts count-words (clojure.string/split text #"\s+")))
언제 사용할까 (When to use)
이 연산들의 reducer 형태를 다음에 사용하세요:
- 다단계 변환의 효율적인 eager 적용
- 매달린 I/O 리소스 문제 회피(지연 시퀀스에서 보이는 것처럼)
다음에 fold를 사용하세요:
- 소스 데이터를 생성하고 메모리에 담을 수 있을 때
- 수행할 작업이 계산(계산 작업이고 I/O나 블로킹이 아닐 때)일 때
- 데이터 항목 수나 수행할 작업이 "크다"고 할 때