Home [Clojure] Do Things: A Clojure Crash Course - Data Structures
Post
Cancel
Preview Image

[Clojure] Do Things: A Clojure Crash Course - Data Structures

모든 Clojure의 data structures는 불변(immutable)하다.

Numbers

Clojure는 꽤나 정교한 수치지원을 가지고 있다.

만약 기술적인 디테일이 궁금하다면 https://clojure.org/reference/data_structures#Data Structures-Numbers 을 방문해보자.

그냥 편하게 쓰면 되는 것 같다.

1
2
3
93
1.2
1/5

Strings

쌍따옴표(double quotes)로 감싸면 된다. 홑따옴표(single quote)는 올바른 문자열이 아니다.

1
2
3
"Lord Voldemort"
"\"He who must not be named\""
"\"Great cow of Moscow!\" - Hermes Conrad"

또 Clojure는 string interpolation이 없기 때문에, 문자열끼리 이어붙일 때는 str 함수를 쓰면된다.

1
2
3
(def name "Chewbacca")
(str "\"Uggllglglglglglglglll\" - " name)
; => "Uggllglglglglglglglll" - Chewbacca

Maps

Maps는 다른 언어에서 dictionaries 나 hashes와 비슷하다.

Clojure에서 map의 종류는 hash maps와 sorted maps 두 가지가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
; empty map:
{}

; :first-name 과 :last-name keywords
{:first-name "Charlie"
	:last-name "McFishwich"}

; associate "string-key" with the + function:
{"string-key" +}

; nested maps
{:name {:first "John" :middle "Jacob" :last "Jingleheimerschmidt"}}

map values 로는 어떤 타입이든 될 수 있다.

  • strings, numbers, maps, vectors, even functions

hash-map function을 사용해서 map도 만들 수 있다.

1
2
(hash-map :a 1 :b 2)
; => {:a 1 :b 2}

키에 대한 값을 가져오고 싶다면, get function을 쓰면 된다.

1
2
3
4
5
(get {:a 0 :b 1} :b)
; => 1

(get {:a 0 :b {:c "ho hum"}} :b)
; => {:c "ho hum"}

만약 해당 키가 존재하지 않는 다면 nil을 리턴한다.

만약 해당 키가 존재하지 않는데, 기본 값을 주었다면, 그 기본값을 리턴해준다.

1
2
3
4
5
(get {:a 0 :b 1} :c)
; => nil

(get {:a 0 :b 1} :c "unicorns?")
; => "unicorns?"

그러면 nested map 인 경우는 어떻게 값을 찾아야 할까?

get-in function을 사용하면 된다.

1
2
(get-in {:a 0 :b {:c "ho hum"}} [:b :c])
; => "ho hum"

map의 키에 해당하는 값을 보는 다른 방법은, map을 function 처럼 다뤄서, key를 argument처럼 사용하는 방식이다.

1
2
({:name "The Human Coffeepot"} :name)
; => "The Human Coffeepot"

Keywords

Clojure의 keywords는 어떻게 사용되는지를 보면 쉽게 이해가 간다.

그들은 앞 section에서 본 것처럼 maps안의 키처럼 주로 사용된다.

아래는 키워드들의 몇 가지 예시다.

1
2
3
4
:a
:rumplestiltsken
:34
:_?

키워드는 data structure 안의 함수에서 대응하는 값 보는데 사용될 수 있다.

1
2
3
4
5
6
7
(:a {:a 1 :b 2 :c 3})
; => 1

; This is equivalent to:

(get {:a 1 :b 2 :c 3} :a)
; => 1

get에서와 같이 기본 값도 줄 수 있다.

1
2
(:d {:a 1 :b 2 :c 3} "No gnome knows homes like Noah knows")
; => "No gnome knows homes like Noah knows"

Vectors

벡터는 array와 비슷하다.

0부터 인덱스가 시작되는 collection이다.

1
2
3
4
5
6
7
8
9
[3 2 1]

; get by index
(get [3 2 1] 0)
; => 3

; another example of getting by index
(get ["a" {:name "Pugsley Winterbottom"} "c"] 1)
; => {:name "Pugsley Winterbottom"}

벡터의 원소는 어떤 타입도 될 수 있고, 섞어서 써도 된다.

그리고 get function 을 통해서 map에서와 같이 값을 볼 수 있다.

또 vector functions을 통해서도 생성 가능하다.

1
2
(vector "creepy" "full" "moon")
; => ["creepy" "full" "moon"]

conj function을 통해서 벡터에 값을 추가할 수 있으며, 이때는 맨 뒤에 추가된다.

1
2
(conj [1 2 3] 4)
; => [1 2 3 4]

벡터는 순서를 저장하는 유일한 방법은 아니며, Clojure는 lists 또한 가지고 있다.

Lists

리스트는 값들의 linear collections 라는 점에서 벡터와 비슷하지만, get을 통해서 값을 가져올 수 없다는 것처럼 조금 차이점이 있다.

list literal 을 쓰려면, ()안에 원소들을 넣고, 맨 앞에 홑따옴표(single quote, ```)을 붙여주면 된다.

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

REPL 에서 리스트를 출력할 때는 홑따옴표가 출력되지 않는다는점을 유의하자.

왜 그런지는 7장에서 밝혀진다.

만약 리스트에서 값을 구하고 싶다면 nth function을 사용하면 된다.

1
2
3
4
5
(nth '(:a :b :c) 0)
; => :a

(nth '(:a :b :c) 2)
; => :c

이 책에서는 언어에 익숙해질 때까지 퍼포먼스에 집중하는 것은 유용하지 않다고 생각하기 때문에 퍼포먼스에 대해 자세히 다루지 않는다.

그러나 벡터에서 get을 통해 값을 찾는 것과 리스트에서 nth를 통해서 값을 찾는 것과 비교하면, 확실히 리스트에서 nth를 통해서 찾는게 더 느리다.

왜냐하면 Clojure에서는 리스트의 모든 n 개의 원소들을 탐색한 후 가져오지만, 벡터의 경우 인덱스를 통해서 바로 가져오기 때문이다.

리스트 또한 값으로 어떤 타입이 가능하며, list 라는 function을 통해서도 생성가능하다.

1
2
(list 1 "two" {3 4})
; => (1 "two" {3 4})

만약 원소를 추가하게되면, 리스트의 앞에 추가된다.

1
2
(conj '(1 2 3) 4)
; => (4 1 2 3)

그럼 언제 리스트를 쓰고, 언제 벡터를 써야할까?

만약 쉽게 값을 맨 앞에 넣고 싶거나, macro를 작성한다면, 리스트를 써야한다.

그렇지 않으면 벡터를 쓰면 된다.

Sets

Sets(이하 집합)은 유일한 원소들의 collection 이다.

Clojure는 두 종류의 집합을 제공하는데, hash sets와 sorted sets이다.

여기서는 hash sets에 대해서 좀 더 알아보겠다.

literal notation은 다음과 같다.

1
#{"kurt vonnegut" 20 :icicle}

hash-set 을 이용해서도 생성가능하다.

1
2
(hash-set 1 1 2 2)
; => #{1 2}

값을 새로 추가하려고 해도 이미 있으면 추가되지 않는다.

1
2
(conj #{:a :b} :b)
; => #{:a :b}

이미 존재하는 리스트와 벡터를 set function을 이용해서 집합으로 만들 수 있다.

1
2
(set [3 3 3 4 4])
; => #{3 4}

contain? function 은 집합에서 원소를 포함하고 있는지 여부를 알려준다.

1
2
3
4
5
6
7
8
(contains? #{:a :b} :a)
; => true

(contains? #{:a :b} 3)
; => false

(contains? #{nil} nil)
; => true

키워드나, get을 사용할 때는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
(:a #{:a :b})
; => :a

(get #{:a :b} :a)
; => :a

(get #{:a nil} nil)
; => nil

(get #{:a :b} "kurt vonnegut")
; => nil

get을 통해서 집합이 nil을 포함하는지 테스트할 때 항상 nil을 리턴하게 되는데

  • 있으면, 그 값(nil)을, 없으면 nil을

이는 헷갈린다. 그러니 이럴 때는 contain? 를 사용하면 된다.

Simplicity

지금까지 데이터 구조 처리에는 새로운 유형이나 클래스를 생성하는 방법이 포함되어 있지 않은데, 이는 단순성에 대한 Clojure의 강조는 기본 제공 데이터 구조에 먼저 도달하도록 되어있기 때문이다.

객체 지향에서는 이런 방법을 이상하다고 생각할 지 모르겠으나, 데이터가 유용하고 이해하기 쉽도록 클래스와 밀접하게 묶일 필요는 없다는 것을 알게 될 것이다.

다음은 Clojure 철학을 암시하는 Clojurists 들에게 사랑받은 epigram이다.

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. —Alan Perlis

10개의 데이터 구조에서 10개의 함수가 작동하는 것보다 하나의 데이터 구조에서 100개의 함수가 작동하는 것이 더 좋습니다.

다음 장에서 Clojure의 철학의 이러한 측면에 대해서 자세히 알아볼 예정이다.

지금은 기본 데이터 구조를 고수하여 코드 재사용성을 확보하는데 노력해보자.

이것으로 Clojure 데이터 구조 입문서를 마치고, 함수를 알아보도록 하자.

References

This post is licensed under CC BY 4.0 by the author.

[Clojure] Do Things: A Clojure Crash Course - Syntax

로컬 개발에서 HTTPS를 사용하는 방법