-
Notifications
You must be signed in to change notification settings - Fork 1
[프로그래밍 클로저] 4장 데이터를 시퀀스로 다루기
다음 링크를 잘 봐두자 :) http://clojure.org/sequences
- 모든 시퀀스에 적용할 수 있는 다양한 기능을 제공.
- 시퀀스 라이브러리 함수의 4가지
- 시퀀스를 생성하는 함수
- 시퀀스를 필터링 하는 함수
- 시퀀스를 서술식(predicate)
- 시퀀스를 변환하는 함수
- 시퀀스는 변경 불가 & 대부분의 시퀀스함수는 새로운 시퀀스를 생성
-
range
(range start? end step?)
- start부터 end까지 step간격으로 변화하는 시퀀스 생성
- 결과: start값은 포함하지만 end값은 포함 안함
- start생략하면 디폴트로 0, step생략하면 1
(range 10) => (0 1 2 3 4 5 6 7 8 9) (range 10 20) => (10 11 12 13 14 15 16 17 18 19) (range 1 25 2) => (1 3 5 7 9 11 13 15 17 19 21 23)
-
repeat
(repeat n x)
- x 원소 n번
(repeat 5 1) => (1 1 1 1 1) (repeat 5 'a') => (a' a' a' a' a')
-
iterate
(iterate f x)
- range의 무한 확장판
- x에서 시작해서 그 값에 함수 f를 적용해 다음 값을 이어 나감
- 결과 값을repl에서 보려면 다른 함수
take
함수의 도움이 필요
(iterate inc 1) => 시퀀스 자체는 무한하지만 repl에서 볼수가 없음, 다음과 같이 take함수의 도움을 받아야. (take 10 (iterate inc 1)) => (1 2 3 4 5 6 7 8 9 10)
-
take
(take n sequence)
- 보통 무한한 컬렉션의 일부를 보기 위해 사용
- 인자로 받은 sequence의 처음 n개만 반환
-
자연수의 시퀀스는 여기저기 필요해서, defn을 이용해 따로 저장해 두면 좋음
(defn whole-numbers [] (iterate inc 1))
=> #'user/whole-numbers
(take 10 (whole-numbers))
=> (1 2 3 4 5 6 7 8 9 10)
-
다시~ repeat으로 가서 인자 하나만 받으면
(repeat x)
- 무한개의 시퀀스 만듦
(take 30 (repeat 1))
-
cycle
(cycle coll)
- 컬렉션을 받아 무한반복
(take 10 (cycle (range 3))) => (0 1 2 0 1 2 0 1 2 0)
-
interleave
(interleave & colls)
- 여러 컬렉션을 받아서, 어느 컬렉션의 끝에 도달할 때 까지 각 컬렉션의 값이 교차되는 새로운 컬렉션을 만듦.
- 받은 컬렉션 중에 하나의 값이 다 소모되면, 값 교차를 끝내기 때문에 유한 컬렉션과 무한 컬렉션을 함께 인자로 넘길 수 있음
(interleave (whole-numbers) ["A" "B" "C" "D" "E"]) => (1 "A" 2 "B" 3 "C" 4 "D" 5 "E") ; 그냥 궁금해서 무한끼리 넘겨봄, take해야 볼수 있 (take 10 (interleave (whole-numbers) (whole-numbers))) => (1 1 2 2 3 3 4 4 5 5)
-
interpose `(interpose seperator coll)
- interleave와 비슷
- 컬렉션과 구분자를 인자로 받아, 컬렉션 각 원소 사이에 구분자를 삽입한 새 시퀀스 생성
(interpose "," ["apples" "bananas" "grapes"]) => ("apples" "," "bananas" "," "grapes");구문자로 나뉜 문자열
-
(apply str...)
을interpose
와 함께 사용하면 손쉽게 원하는 문자열을 만들 수 있다. (책에 오타가 있음appy.. 근데 이전에 나온적 있었나용?)
(apply str (interpose \, ["apples" "bananas" "grapes"])) => "apples,bananas,grapes"
- (apply str ...)이 매우 자주 쓰이기 떄문에 clojure-contrib라이브러리에서는 str-join이라는 이름으로 추상화하고 있다./
(str-join seperator sequence)
; 이게 지금은 str-join아니라 join이고 use도 다음과 같이 수정해야함 (use '[clojure.string :only (join)]) (join \, ["apples" "bananas" "grapes"]) =>"apples,bananas,grapes"
-
임의 개수의 인자 받아서 원하는 타입의 컬렉션 만들어내는 함수 존재
(list & elements)
(vector & elements)
(hash-set & elements)
(hash-map & key-1 val-1 ...)
-
set
- hash-set의 사촌뻘
- 컬렉션을 인자로 받음
(set [1 2 3]) => #{1 3 2}
-
hash-set
- 컬렉션 대신 임의 개수 인자를 받음
(hash-set 1 2 3) => #{1 3 2}
-
vec
- vector사촌
- 임의 개수 인자 대신 하나의 컬렉션을 인자로 받음
(vector 0 1 2) => [0 1 2] (vec (range 3)) => [0 1 2]
여기까지가 생성~
- filter
(filter pred coll)
- 서술식과 컬렉션은 인자로 받음
- 컬렉션 가운데 서술식을 만족하는 원소만으로 된 시퀀스 반환
- 짝수 혹은 홀수 시퀀스 예
(take 10 (filter even? (whole-numbers))) => (2 4 6 8 10 12 14 16 18 20) (take 10 (filter odd? (whole-numbers))) => (1 3 5 7 9 11 13 15 17 19)
- take-while
take-while pred coll
- 서술식을 컬렉션의 각 원소에 적용해, 거짓이 나타나기 전까지의 원소로만 이루어진 시퀀스 리턴
- 거짓이 반화되는 원소를 만나는 순간 그 이후 컬렉션은 버려집
- 첫번째 모음이 나타나기 전의 모든 문자를 얻고 싶을 때의 예
(take-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox") => (\t \h) ; \얘는 뭐지? ; * 집합은 그 자체로 함수가 됨, #{\a\e\i\o\u}를 '모음의 집합'도 되지만 '인자가 모음인지를 판단하는 함수' ; complement : 서술식의 값을 반대로 뒤집는 것. complement를 이용해 모음이 아닌 글자를 찾는 서술식을 만들어 사용하고 있다 (뭔소리야?) ; 실험 (take-while (complement #{"a" "e" "i" "o" "u"}) "the-quick-brown-fox") => (\t \h \e \- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x) (take-while #{\a\e\i\o\u} "the-quick-brown-fox") => ()
- drop-while
drop-while pred coll
- take-while반대
- 앞부분 부터 서술식을 참으로 만드는 원소를 제거
- 문장에서 첫모음 전의 모든 자음을 제거하는 방법
(drop-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox") => (\e \- \q \u \i \c \k \- \b \r \o \w \n \- \f \o \x)
- split-at
(split-at index coll)
, split-with(split-with pred coll)
- split-at은 인덱스를 받고, split-with는 서술식을 인자로 받는다
(split-at 5 (range 10)) => [(0 1 2 3 4) (5 6 7 8 9)] (split-with #(<= % 10) (range 0 20 2)) => [(0 2 4 6 8 10) (12 14 16 18)]
take,split,drop으로 시작하는 함수는 모두 평가가 지연된 Lazy sequence를 반환한다.
필터링 함수는 서술식을 받아 시퀀스를 반환함.
시퀀스 서술식은 다른 서술식을 받아 그 서술식을 시퀀스에 어떤 식으로 적용할지를 결정.
-
every?
(every? pred coll)
- 인자로 받은 서술식을 시퀀스의 각 원소에 적용한 결과가 모두 참일 때만 참을 반환
(every? odd? [1 3 5]) => true (every? odd? [1 3 5 8]) => false
-
some
some pred coll
- 인자로 받은 서술식을 매우 느슨하게 적용
- 서술식을 적용한 결과중 첫번째로 거짓이 아닌 값을 반환(즉 예에서는 하나라도 참이면 true를 리턴, identity에서는 1을 리턴, 모두 거짓이면 nil
- 물음표로 끝나지 않음
- 서술식과 유사하게 사용되지만 서술식은 아님. 처음 서술식이 만족되면 true대신에 서술식이
적용된 결과
를 리턴하기 때문
(some even? [1 2 3]) => true (some even? [1 3 5]) => nil (some identity [nil false 1 nil 2]) => 1
-
not-every?
(not-every? pred coll)
- 자연수가 모두 짝수는 아님!
(not-every? even? (whole-numbers)) => true
-
not-any?
(not-any? pred coll)
- 어떤 자연수도 짝수가 아니라고 하면 뻥!
(not-any? even? (whole-numbers)) => false
무한 컬렉션에 서술식 적용할 땐 주의! 멈추지 않을 수 있음
- map
- reduce
- sort
- sort-by
-
클로저의 시퀀스는 대부분 연산을 지연한다.
-
지연 시퀀스는 다음과 같은 장점을 가진다.
- 리소스가 많이 필요한 연산을 필요할 때까지 미룰 수 있다.
- 메모리 용량을 초과하는 시퀀스를 다룰 수 있다.
- I/O도 지연 시킬 수 있다.
-
무한 정수
(def integer (iterate inc 1))
-
클로저는 대부분의 시퀀스 함수에서 지연된 시퀀스를 리턴한다.
-
지연 시퀀스를 모두 실행하기 위해서
doall
,dorun
을 사용한다.(def x (for [i (range 1 3)] (do (println i) i))) (doall x) -> 1 2 (1 2)
-
dorun
은 이전에 방문한 원소를 메모리에 보관하지 않기 때문에 큰 시퀀스를 모두 실행하기 좋다. 하지만 전체 결과를 얻을 수는 없다.(dorun x) -> 1 2 nil ; 지난 내용이 없기 때문에 nil을 리턴한다.
-
dorun
과dorun
은 부수효과를 일으키는 함수이며 사용할 일이 드물다. (최종 결과를 화면에 전달할때)
-
자바 클래스들 중 다음과 같은 것들이 시퀀스로 취급된다.
- 컬렉션 API
- 정규식
- 파일 시스템
- XML
- 관계형 데이터베이스
-
자바 배열을 시퀀스로 다루기
(first (.getBytes "abc")) -> 97
-
자바 해시맵을 시퀀스로 다루기
(first (System/getProperties)) -> #<Entry java.runtime.name=Java(TM) SE Runtime Environment>
-
자바 문자열을 시퀀스로 다루기
(first "Hello") -> \H
-
그래도 자바 컬렉션 보다 클로저 컬렉션(Map, Set, List등)을 사용하는 것이 좋다.
-
자바 정규식을 시퀀스로 다루기
(first (re-seq #"\w+" "the quick brown fox")) -> the
-
파일 시스템을 시퀀스로 다루기
(import java.io.File) ; 현재 디렉토리에 있는 파일 이름을 출력한다. (map #(.getName %) (.listFiles (File. "."))) -> (".git" "./[프로그래밍-클로저]-4장-데이터를-시퀀스로-다루기.md") ; 현재 디렉토리의 하위에 있는 파일의 갯수를 출력한다. (count (file-seq (File. "."))) ; 최근 30분이내에 변경된 파일만 출력한다. (defn minites-to-millis [mins] (* mins 1000 60)) (defn recently-modified? [file] (> (.lastModified file) (- (System/currentTimeMillis) (minites-to-millis 30)))) (filter recently-modified? (file-seq (File. "."))) -> (#<File ./[프로그래밍-클로저]-4장-데이터를-시퀀스로-다루기.md>)
-
스트림을 시퀀스로 다루기
; 현재 디렉토리 하위에 있는 클로저 파일들의 라인수를 출력 (import java.io.File) (use 'clojure.java.io) (defn non-blank? [line] (if (re-find #"\S" line) true false)) (defn non-git? [file] (not (.contains (.toString file) ".git"))) (defn clojure-source? [file] (.endsWith (.toString file) ".clj")) (defn clojure-loc [base-file] (reduce + (for [file (file-seq base-file) :when (and (clojure-source? file) (non-git? file))] (with-open [rdr (reader file)] (count (filter non-blank? (line-seq rdr))))))) (clojure-loc (File. ".")) -> 6
-
XML을 시퀀스로 다루기
(use 'clojure.xml) (parse (java.io.ByteArrayInputStream. (.getBytes "<a>1</a>"))) -> {:tag :a, :attrs nil, :content ["1"]}
-
리스트에 대한 함수
(peek '(1 2 3)) -> 1 (pop '(1 2 3)) -> (2 3) ; peek은 first와 같지만 pop은 rest와 같지 않다. (rest ()) -> () (pop ()) -> java.lang.IllegalStateException: Can't pop empty list
-
벡터에 대한 함수
; 벡터 역시 peek와 pop 함수를 지원한다. ; 벡터의 끝이 기준이 된다는 점이 다르다. (peek [1 2 3]) -> 3 (pop [1 2 3]) -> [1 2] (get [:a :b :c] 1) -> :b (get [:a :b :c] 5) -> nil ;벡터는 그 자체로 함수이기도 하다. ([:a :b :c] 1) -> :b ([:a :b :c] 5) -> java.lang.ArrayIndexOutOfBoundsException: 5 ; assoc은 특정 인덱스에 새 값을 집어넣는다. (assoc [0 1 2 3 4] 2 :two) -> [0 1 :two 3 4] ; subvec은 기존 벡터의 일부를 반환한다. (subvec [1 2 3 4 5] 1 3) -> [2 3] ; end가 명시되지 않으면, 자동으로 벡터의 끝이 end가 된다. (subvec [1 2 3 4 5] 3) -> [4 5] ; take와 drop을 사용해도 subvec을 흉내 낼 수 있다. ; take와 drop은 어떤 시퀀스에도 사용할 수 있다. ; subvec은 벡터에만 사용할 수 있는 대신 실행 속도가 훨씬 빠르다. ; 기존의 시퀀스 함수와 기능이 겹치는 특정 자료구조용 함수가 있다면, 그 함수는 실행 속도 때문에 존재하는 것일 가능성이 높다. (take 2 (drop 1 [1 2 3 4 5])) -> (2 3)
-
맵에 대한 함수
(keys {:sundance "spaniel", :darwin "beagle"}) -> (:sundance :darwin) (vals {:sundance "spaniel", :darwin "beagle"}) -> ("spaniel" "beagle") (get {:sundance "spaniel", :darwin "beagle"} :darwin) -> "beagle" (get {:sundance "spaniel", :darwin "beagle"} :snoopy) -> nil ; 맵 역시 키를 인자로 받는 함수다. ({:sundance "spaniel", :darwin "beagle"} :darwin) -> "beagle" ({:sundance "spaniel", :darwin "beagle"} :snoopy) -> nil ; 키워드 역시 함수다. (:darwin {:sundance "spaniel", :darwin "beagle"}) -> "beagle" (:snoopy {:sundance "spaniel", :darwin "beagle") -> nil ; nil을 값으로 가지는 맵 (def score {:stu nil :joey 100}) (:stu score) -> nil (contains? score :stu) -> true ; 키에 해당하는 값이 없을 경우 반환할 값을 get의 세 번째 인자로 넘긴다. (get score :stu :score-not-found) -> nil (get score :aaron :score-not-found) -> :score-not-found
- 새로운 맵을 생성하는 함수
- assoc: 기존 맵에 새로운 키/값 쌍이 더해진 맵을 반환한다.
- dissoc: 기존 맵에 새로운 키/값 쌍을 제거한 맵을 반환한다.
- select-keys: 특정한 키에 대한 값만 남긴 맵을 반환한다.
- merge: 맵들을 합친다. 여러 맵이 같은 키를 가질 경우, 가장 오른쪽 맵의 키/값이 살아남는다.
(def song {:name "Agnus Dei" :artist "Krzysztof Penderecki" :album "Polish Requiem" :genre "Classical"}) (assoc song :kind "MPEG Audio File") -> {:name "Agnus Dei", :album "Polish Requiem", :kind "MPEG Audio File", :genre "Classical", :artist "Krzysztof Penderecki"} (dissoc song :genre) -> {:name "Agnus Dei", :album "Polish Requiem", :artist "Krzysztof Penderecki"} (select-keys song [:name :artist]) -> {:name "Agnus Dei", :artist "Krzysztof Penderecki"} (merge song {:size 8118166, :time 507245}) -> {:name "Agnus Dei", :album "Polish Requiem", :genre "Classical", :size 8118166, :artist "Krzysztof Penderecki", :time 507245} ; song 자체는 결코 변하지 않는다.
; 둘 이상의 맵이 같은 키를 가질 경우, 그 키들의 값을 어떻게 조합해서 키에 대한 값을 만들어 낼지 함수로 지정할 수 있다. (merge-with concat {:rubble ["Barney"], :flintstone ["Fred"]} {:rubble ["Betty"], :flintstone ["Wilma"]} {:rubble ["Bam-Bam"], :flintstone ["Pebbles"]}) -> {:rubble ("Barney" "Betty" "Bam-Bam"), :flintstone ("Fred" "Wilma" "Pebbles")}
- 새로운 맵을 생성하는 함수
-
집합에 대한 함수
(use 'clojure.set) (def languages #{"java" "c" "d" "clojure"}) (def letters #{"a" "b" "c" "d" "e"}) (def beverages #{"java" "chai" "pop"})
- clojure.set에 속한 함수 가운데 첫 번째 부류: 집합 이론에 있는 연산을 수행
- union: 합집합을 반환한다.
- intersection: 교집합을 반환한다.
- difference: 차집합을 반환한다.
- select: 서술식을 만족시키는 모든 원소의 집합을 반환한다.
(union languages beverages) -> #{"java" "c" "d" "clojure" "chai" "pop"} (difference languages beverages) -> #{"c" "d" "clojure"} (intersection languages beverages) -> #{"java"} ; 글자 하나로만 이루어진 언어 (select #(= 1 (.length %)) languages) -> #{"c" "d"}
(def compositions #{{:name "The Art of the Fugue" :composer "J. S. Bach"} {:name "Musical Offering" :composer "J. S. Bach"} {:name "Requiem" :composer "Giuseppe Verdi"} {:name "Requiem" :composer "W. A. Mozart"}}) (def composers #{{:composer "J. S. Bach" :country "Germany"} {:composer "W. A. Mozart" :country "Austria"} {:composer "Giuseppe Verdi" :country "Italy"}}) (def nations #{{:nation "Germany" :language "German"} {:nation "Austria" :language "German"} {:nation "Italy" :language "Italian"}}) ; 재명명 함수는 기존 이름과 새 이름으로 이루어진 맵을 기반으로 해서 키의 이름을 바꾼다. (rename compositions {:name :title}) -> #{{:title "The Art of the Fugue" :composer "J. S. Bach"} {:title "Musical Offering" :composer "J. S. Bach"} {:title "Requiem" :composer "Giuseppe Verdi"} {:title "Requiem" :composer "W. A. Mozart"}}) (select #(= (:name %) "Requiem") compositions) -> #{{:name "Requiem", :composer "W. A. Mozart"} {:name "Requiem", :composer "Giuseppe Verdi"}} (project compositions [:name]) -> #{{:name "Musical Offering"} {:name "Requiem"} {:name "The Art of the Fugue"}} ; :composer 키를 공유하는 악곡과 작곡가 사이의 조합을 얻는 코드다. (join compositions composers) -> #{{:name "Requiem" :country "Austria", :composer "W. A. Mozart"} {:name "Musical Offering", :country "Germany", :composer "J. S. Bach"} {:name "Requiem" :country "Italy", :composer "Giuseppe Verdi"} {:name "The Art of the Fugue", :country "Germany", :composer "J. S. Bach"}} ; 두 관계 사이에 대응되는 키를 맵으로 지정할 수 있다. (join composers nations {:country :nation}) -> #{{:language "German", :nation "Austria", :composer "W. A. Mozart", :country "Austria"} {:language "German", :nation "Germany", :composer "J. S. Bach", :country "Germany"} {:language "Italian", :nation "Italy", :composer "Giuseppe Verdi", :country "Italy"} (project (join (select #(= (:name %) "Requiem") compositions) composers) [:country]) -> #{{:country "Italy"} {:country "Austria"}}
- clojure.set에 속한 함수 가운데 첫 번째 부류: 집합 이론에 있는 연산을 수행