데이터 구조 (Data Structures)
데이터 구조 (Data Structures)
Clojure에는 풍부한 데이터 구조가 있어요. 모두 불변(immutable)·영속(persistent) 이라는 공통 속성을 공유해요. 공식 Reference 기준으로 기본 값 타입과 리스트·벡터·맵·셋 컬렉션을 정리할게요.
본문
Clojure의 데이터 구조는 다음 속성을 함께 갖고 있어요.
- 불변이다
- 리더로 다시 읽을 수(read-able) 있다
equals구현에서 올바른 값 동등성 시맨틱을 지원한다- 좋은 해시 값을 제공한다
그리고 컬렉션은 추가로:
- 인터페이스로 다뤄진다
- 시퀀싱(sequencing)을 지원한다
- 영속적(persistent) 조작을 지원한다
- 메타데이터를 지원한다
java.lang.Iterable을 구현한다java.util.Collection또는java.util.Map의 필수(읽기 전용) 부분을 구현한다
nil
nil은 Clojure의 모든 데이터 타입에서 가질 수 있는 값이에요. Java의 null과 같은 값이고, 조건 시스템은 nil과 false를 기준으로 해요 — 조건 테스트에서 논리 거짓은 nil과 false이고, 그 외는 전부 논리 참이에요. 또 nil은 시퀀스 프로토콜에서 시퀀스 끝맺음 센티널 값으로도 쓰여요.
숫자 (Numbers)
Clojure는 기본적으로 JVM 원시 값(primitive)을 온전히 지원해 숫자 애플리케이션에서 고성능·관용적 코드를 짤 수 있어요. java.lang.Number에서 파생된 박싱 숫자 타입(BigInteger, BigDecimal)과 자체 Ratio 타입도 지원해요.
- Long — 기본적으로 자연수는 Java
long원시 타입 인스턴스로 동작해요. 원시 정수 연산이 원시 값에 담기엔 너무 큰 값을 내면java.lang.ArithmeticException이 던져져요. 아포스트로피가 붙은 연산자+'-'*'inc'dec'는 오버플로 시 자동으로 BigInt로 승격하지만 일반 연산자보다는 비효율적이에요. - Ratio — 정수 사이의 비율을 나타내요. 정수로 환원되지 않는 정수 나눗셈은 부동소수점이나 잘린 값이 아니라 비율로 나와요. 즉
22/7 = 22/7이에요. - 전염(Contagion) — BigInt와 부동소수점 타입은 연산을 가로질러 "전염"돼요. BigInt가 포함된 정수 연산은 BigInt 결과를,
double이나float가 포함된 연산은double결과를 내요. - BigInt/BigDecimal 리터럴 — 뒤에 각각
N과M접미사를 붙여요.
| 예시식 | 반환값 |
|---|---|
(== 1 1.0 1M) |
true |
(/ 2 3) |
2/3 |
(/ 2.0 3) |
0.6666666666666666 |
(map #(Math/abs %) (range -3 3)) |
(3 2 1 0 1 2) |
(class 36786883868216818816N) |
clojure.lang.BigInt |
(class 3.14159265358M) |
java.math.BigDecimal |
관련 함수 — 계산:
+-*/incdecquotremminmax/ 자동 승격 계산:+'-'*'inc'dec'/ 비교:==<>>=zero?pos?neg?/ 비트 연산:bit-andbit-orbit-xorbit-notbit-shift-rightbit-shift-left/ Ratio:numeratordenominator/ 변환:intbigdecbigintdoublefloatlongnumshort
문자열 (Strings)
Clojure 문자열은 Java String이에요.
user=> (map (fn [x] (.toUpperCase x)) (.split "Dasher Dancer Prancer" " "))
("DASHER" "DANCER" "PRANCER")
관련 함수 —
strstring?pr-strprn-strprint-strprintln-strwith-out-str문자 관련 —charchar-name-stringchar-escape-string
키워드 (Keywords)
키워드는 자기 자신으로 평가되는 상징적 식별자예요. 매우 빠른 동등성 테스트를 제공해요. 심볼과 마찬가지로 이름과 선택적 네임스페이스를 갖고, 둘 다 문자열이에요. 앞의 :는 네임스페이스나 이름에 포함되지 않아요. 키워드는 한 인자(맵)와 선택적 두 번째 인자(기본값)를 받는 IFn을 구현해요. (:mykey my-hash-map :none)은 (get my-hash-map :mykey :none)과 같아요.
관련 함수 —
keywordkeyword?
심볼 (Symbols)
심볼은 보통 다른 무언가를 가리키는 데 쓰는 식별자예요. 프로그램 폼에서 함수 파라미터, let 바인딩, 클래스 이름, 클래스 멤버, 전역 var를 가리키는 데 쓰여요. 이름과 선택적 네임스페이스를 갖고 문자열이에요. 메타데이터를 가질 수 있어요(with-meta). 키워드와 마찬가지로 한 인자(맵)와 선택적 기본값 인자를 받는 IFn을 구현해요.
관련 함수 —
symbolsymbol?gensym(#-접미사 리더 매크로 참고)
컬렉션 (Collections)
Clojure 컬렉션은 전부 불변이고 영속적이에요. 특히 구조 공유(structural sharing) 를 활용해 '변형된' 버전을 효율적으로 만들고, 영속적 사용에 대한 성능 보장을 지켜요. 컬렉션은 효율적이고 본질적으로 스레드 안전해요. 컬렉션은 추상화로 표현되고 하나 이상의 구체적 실현(realization)이 있을 수 있어요. '변형' 연산이 새 컬렉션을 내므로 원본과 다른 구체 타입일 수 있지만, 같은 논리적(인터페이스) 타입이에요.
모든 컬렉션은 크기를 얻는 count, '추가'하는 conj, 전체를 훑는 시퀀스를 얻는 seq를 지원하며, 컬렉션 타입에 따라 세부 동작이 조금씩 달라요. 컬렉션이 seq를 지원하므로 모든 시퀀스 함수를 어떤 컬렉션에도 쓸 수 있어요.
해시 — Clojure 컬렉션은 hashCode() 구현에서 Java 컬렉션 인터페이스가 지정한 알고리즘을 따르고, 더 나은 해시 속성을 주는 자체 hasheq 값도 계산해요. 순서 있는 컬렉션(벡터·리스트·seq 등)과 무순서 컬렉션(맵·셋)은 각각 다음 알고리즘으로 hasheq를 계산해요 (hash는 hasheq를 계산).
(defn hash-ordered [collection]
(-> (reduce (fn [acc e] (unchecked-add-int
(unchecked-multiply-int 31 acc)
(hash e)))
1
collection)
(mix-collection-hash (count collection))))
(defn hash-unordered [collection]
(-> (reduce unchecked-add-int 0 (map hash collection))
(mix-collection-hash (count collection))))
mix-collection-hash알고리즘은 변경될 수 있는 구현 세부 사항이에요.
리스트 (Lists, IPersistentList)
리스트는 컬렉션이고 ISeq 인터페이스를 직접 구현해요. (빈 리스트도 ISeq를 구현하지만, seq 함수는 빈 시퀀스에 대해 항상 nil을 돌려줘요.) count는 O(1)이고, conj는 항목을 리스트 맨 앞에 넣어요.
관련 함수 — 만들기:
listlist*/ 스택처럼 다루기:peekpop/ 확인:list?
벡터 (Vectors, IPersistentVector)
벡터는 연속된 정수로 인덱스되는 값들의 컬렉션이에요. 인덱스로 항목에 log32N 홉으로 접근할 수 있어요. count는 O(1)이고, conj는 항목을 벡터 끝에 넣어요. 역순 항목을 돌려주는 rseq도 지원해요. 벡터는 한 인자를 받는 IFn을 구현해요 — 그 인자를 인덱스로 간주하고 nth처럼 자기 자신에서 찾아요. 즉 벡터는 자기 인덱스의 함수예요. 벡터 비교는 먼저 길이, 그다음 각 요소를 순서대로 비교해요.
관련 함수 — 만들기:
vectorvecvector-of/ 확인:getnthpeekrseqvector?/ '변경':assocpopsubvecreplace(zippers도 참고)
맵 (Maps, IPersistentMap)
맵은 키를 값에 매핑하는 컬렉션이에요. 해시 맵과 정렬 맵 두 종류가 제공돼요. 해시 맵은 키가 hashCode와 equals를 올바르게 지원해야 하고, 정렬 맵은 키가 Comparable을 구현하거나 Comparator 인스턴스여야 해요. 해시 맵이 더 빠른 접근(log32N 홉)을 제공하지만, 정렬 맵은 말 그대로 정렬돼 있어요. count는 O(1)이에요. conj는 (단일 엔트리일 수도 있는) 다른 맵을 항목으로 받아, 기존 맵에 새 맵의 엔트리를 더한 새 맵을 돌려줘요 (기존 엔트리를 덮어쓸 수 있음). MapEntry나 두 항목(키·값)의 벡터도 받아요. seq는 키·값 쌍인 맵 엔트리들의 시퀀스를 돌려줘요. 정렬 맵은 역순 엔트리를 주는 rseq도 지원해요. 맵은 한 인자(키)와 선택적 기본값 인자를 받는 IFn을 구현해요 — 즉 맵은 자기 키의 함수예요. nil 키와 값은 괜찮아요.
관련 함수 — 만들기:
hash-mapsorted-mapsorted-map-by/ '변경':assocdissocselect-keysmergemerge-withzipmap/ 확인:getcontains?findkeysvalsmap?/ 엔트리 확인:keyval
StructMaps
StructMap 대부분의 용도는 이제 레코드(records) 로 쓰는 게 좋아요. 여러 맵 인스턴스가 같은 기본 키 집합을 가질 때(다른 언어에서 struct나 object로 쓰이는 상황), StructMap은 그 키 정보를 효율적으로 공유하면서 선택적인 고성능 접근자를 제공해요. StructMap은 모든 면에서 맵이라 같은 함수 집합을 지원하고 다른 맵과 상호운용되며 영속적으로 확장 가능해요(기본 키에 제한되지 않음). 유일한 제약은 기본 키 중 하나를 dissoc할 수 없다는 것뿐이고, 기본 키는 순서를 유지해요. create-struct나 defstruct로 구조 기반 객체를 만든 뒤 struct-map이나 struct로 인스턴스를 만들어요.
(defstruct desilu :fred :ricky)
(def x (map (fn [n]
(struct-map desilu
:fred n
:ricky 2
:lucy 3
:ethel 4))
(range 100000)))
(def fred (accessor desilu :fred))
(reduce (fn [n y] (+ n (:fred y))) 0 x)
-> 4999950000
(reduce (fn [n y] (+ n (fred y))) 0 x)
-> 4999950000
관련 함수 — 설정:
create-structdefstructaccessor/ 인스턴스 생성:struct-mapstruct
ArrayMaps
코드 폼을 조작할 때 키 순서를 유지하는 맵이 필요할 때가 있어요. array map은 그런 맵으로, key val key val...의 배열로 구현돼요. 그래서 선형 탐색 성능을 가지며 매우 작은 맵에만 적합해요. 전체 맵 인터페이스를 구현하고 array-map으로 만들 수 있어요. array map은 '변형'되지 않았을 때만 정렬 순서를 유지하고, 이후 assoc하면 결국 해시 맵으로 '변해요'.
셋 (Sets)
셋은 고유 값들의 컬렉션이에요.
#{:a :b :c :d}
-> #{:d :a :b :c}
hash-set과 sorted-set 함수로 셋을 만들고, set 함수로 컬렉션의 값 집합을 얻을 수 있어요.
(hash-set :a :b :c :d)
-> #{:d :a :b :c}
(sorted-set :a :b :c :d)
-> #{:a :b :c :d}
(set [1 2 3 2 1 2 3])
-> #{1 2 3}
셋은 컬렉션이에요.
(def s #{:a :b :c :d})
(conj s :e)
-> #{:d :a :b :e :c}
(count s)
-> 4
(seq s)
-> (:d :a :b :c)
(= (conj s :e) #{:a :b :c :d :e})
-> true
셋은 disj로 '제거'를, contains?와 get을 지원하며, get은 발견하면 키와 동등하게 비교되는 객체를 돌려줘요. 셋은 get을 통한 멤버의 함수이기도 해요.
(disj s :d)
-> #{:a :b :c}
(contains? s :b)
-> true
(get s :a)
-> :a
(s :b)
-> :b
(s :k)
-> nil
Clojure는 union / difference / intersection 같은 기본 셋 연산과, '관계'(단순히 맵들의 셋)에 대한 의사 관계대수 연산 select / index / rename / join을 제공해요.
더 알아보기
- Clojure Reference - Sequences — 컬렉션을 다루는 시퀀스 추상화
- Clojure Reference - Other Useful Functions and Macros — 컬렉션을 다루는 함수
- Clojure API - clojure.core — 전체 데이터 구조 함수 목록