Skip to content

[프로그래밍 클로저] 4장 데이터를 시퀀스로 다루기

Lester Kwak edited this page Aug 20, 2014 · 12 revisions

다음 링크를 잘 봐두자 :) http://clojure.org/sequences

모든 것은 시퀀스 (lester)

시퀀스 라이브러리 사용하기 (joony)

  • 모든 시퀀스에 적용할 수 있는 다양한 기능을 제공.
  • 시퀀스 라이브러리 함수의 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을 리턴한다.
  • dorundorun은 부수효과를 일으키는 함수이며 사용할 일이 드물다. (최종 결과를 화면에 전달할때)

자바 객체 역시 시퀀스 처럼

  • 자바 클래스들 중 다음과 같은 것들이 시퀀스로 취급된다.

    • 컬렉션 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"]}

특정 자료구조용 함수 (lester)

  • 리스트에 대한 함수

    (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"}}