Clojure의 시퀀스(Sequences) — 논리적 리스트로 데이터 다루기

Clojure의 시퀀스(Sequences) — 논리적 리스트로 데이터 다루기

Clojure는 수많은 알고리즘을 시퀀스(seq) 단위로 정의해요. 시퀀스는 '논리적 리스트'라고 볼 수 있어요. 대부분의 Lisp에서는 리스트가 구체적인 2칸(2-slot) 구조로 표현되는데, Clojure는 그 대신 ISeq 인터페이스를 써서 여러 데이터 구조가 자신의 요소들을 시퀀스로 드러낼 수 있게 했죠. 이 글에서는 시퀀스가 무엇이고, first/rest/cons 인터페이스가 어떻게 생겼으며, 시퀀스 라이브러리가 어떤 기준으로 묶이는지 살펴볼게요.

출처: Clojure 공식문서

본문

시퀀스부터 보자

seq 함수는 컬렉션에 맞는 ISeq 구현을 돌려줘요. 그런데 시퀀스는 이터레이터(iterator)와 달라요. 상태를 들고 있는 커서(cursor)가 아니라 영속적(persistent)이고 불변(immutable) 이죠. 그래서 단순한 foreach 이상으로 유용해요. 함수가 시퀀스를 소비하고 만들어낼 수 있고, 스레드에도 안전하며, 구조를 공유할 수도 있어요.

시퀀스 라이브러리의 함수 대부분은 lazy해요. 즉 seq를 돌려주는 함수들은 소비될 때 조금씩, 점진적으로 만들어내고, 그래서 seq 인자들도 점진적으로 소비해요. lazy seq를 돌려주는 함수는 lazy-seq 매크로로 구현할 수 있어요. 자세한 내용은 lazy를 참고해요.

Iterable과 배열 위에서 seq

seqIterable을 구현한 객체에 쓰면, 결과 시퀀스는 여전히 불변이고 영속적이며 데이터를 한 번만 지나가는 단일 패스(single pass) 를 나타내요. 그런데 그 패스가 lazy하게 일어날 수 있기 때문에, seq가 호출된 뒤에 일어난 변경이 그 패스에 보일 수 있어요. 또 뒤의 이터레이터가 ConcurrentModificationException에 노출돼 있다면 결과 시퀀스도 마찬가지로 그 예외에 노출돼요.

네이티브 Java 배열에 seq를 쓰면, 배열이 바뀌는 대로 그 변경이 시퀀스에 그대로 반영돼요. 완전한 불변성을 얻으려면 원본 배열을 직접 복사해야 하죠. 그럼에도 Iterable과 배열 위에 seq를 쓰는 건 여전히 가치가 있어요. 시퀀스는 다중 패스(multi-pass)와 lazy 알고리즘을 지원하니까요. 견고한 프로그램이라면 시퀀스가 걸려 있는 배열이나 Iterable을 변경하지 않는 게 좋아요.

시퀀스 라이브러리가 컬렉션을 다루는 법

시퀀스 라이브러리의 함수 상당수는 하나 이상의 컬렉션을 받아서, 그 위에 seq를 호출한 뒤 결과 시퀀스에 대해 동작해요. 다시 말해, 많은 함수가 컬렉션을 받지만 실제로는 그 시퀀스에 대해 연산한다는 뜻이에요.

The Seq interface

시퀀스 인터페이스는 세 함수로 이루어져 있어요.

(first coll)

컬렉션의 첫 번째 항목을 돌려줘요. 인자에 seq를 호출하고, coll이 nil이면 nil을 돌려줘요.

(rest coll)

첫 번째 항목 뒤의 항목들로 이루어진 시퀀스를 돌려줘요. 인자에 seq를 호출하고, 남은 항목이 없으면 seq가 nil을 돌려주는 그런 논리적 시퀀스를 돌려줘요.

(cons item seq)

item이 첫 요소이고 seq가 나머지인 새 시퀀스를 돌려줘요.

restnext의 차이, 그리고 lazy-seq에 대한 얘기는 lazy 문서에서 다뤄요.

The Seq library

시퀀스 라이브러리는 핵심 함수들의 표본을 기능별로 묶어둔 거예요. 어떤 함수는 여러 방식으로 쓰일 수 있어 여러 그룹에 나타나기도 해요. 전체 목록은 API 섹션에 훨씬 많이 있어요.

참고로 Clojure 1.7부터는 transducers도 제공해요. 컬렉션 변환을 구성하는 또 다른 모델인데, 입력·처리·출력 부분을 분리해서 변환을 더 다양한 맥락(core.async 채널 등)에서 재사용할 수 있게 해주죠. 아래 목록의 시퀀스 함수 중 상당수는 입력 컬렉션을 빼고 호출하면 transducer를 만들어요. 자세한 내용은 Transducers 문서를 봐요.

Seq in, Seq out

시퀀스를 넣어 시퀀스를 얻어내는 함수들이에요.

  • 긴 시퀀스에서 더 짧은 시퀀스: distinct, filter, remove, for, keep, keep-indexed
  • 짧은 시퀀스에서 더 긴 시퀀스: cons, concat, lazy-cat, mapcat, cycle, interleave, interpose
  • 머리(head) 항목이 빠진 시퀀스: rest, next, fnext, nnext, drop, drop-while, nthnext, for
  • 꼬리(tail) 항목이 빠진 시퀀스: take, take-nth, take-while, butlast, drop-last, for
  • 시퀀스 재배열: flatten, reverse, sort, sort-by, shuffle
  • 중첩 시퀀스 만들기: split-at, splitv-at, split-with, partition, partition-all, partition-by, partitionv, partitionv-all
  • 각 항목을 처리해 새 시퀀스 만들기: map, pmap, mapcat, for, replace, reductions, map-indexed, seque

Using a seq

시퀀스를 실제로 사용할 때 쓰는 함수들이에요.

  • 컬렉션이 시퀀스를 만들 수 있는지 확인: seqable?
  • 시퀀스에서 특정 순번의 항목 꺼내기: first, ffirst, nfirst, second, nth, when-first, last, rand-nth
  • 시퀀스로 컬렉션 만들기: zipmap, into, reduce, set, vec, into-array, to-array-2d, frequencies, group-by
  • 시퀀스의 항목들을 함수 인자로 넘기기: apply
  • 시퀀스로부터 불(boolean) 계산하기: not-empty, some, reduce, seq?, every?, not-every?, not-any?, empty?
  • 술어(predicate)로 시퀀스 검색: some, filter
  • lazy 시퀀스 강제 평가: doseq, dorun, doall
  • lazy 시퀀스가 이미 강제 평가됐는지 확인: realized?

Creating a seq

시퀀스를 새로 만들어내는 함수들이에요.

  • 컬렉션에서 lazy 시퀀스: seq, vals, keys, rseq, subseq, rsubseq
  • 생산 함수(producer function)에서 lazy 시퀀스: lazy-seq, repeatedly, iterate
  • 상수(constant)에서 lazy 시퀀스: repeat, range
  • 다른 객체들에서 lazy 시퀀스: line-seq, resultset-seq, re-seq, tree-seq, file-seq, xml-seq, iterator-seq, enumeration-seq

더 알아보기