nashcft's blog

時々何か書く。

Living Clojure 読書メモ

Clojure入門のために “Living Clojure” を読んでいる。
読みっ放しにするのも何なので、適当なところでメモをまとめて記録しておくことにした。

Preface

Chapter 1. The Structure of Clojure

Simple Values

  • Decimal以外にRatioによる表現がある
;; decimal
3.14

;; ratio
1/3

Collections

;; List: 
'(1 2 3)
;; Vector: 
[1 2 3]
;; Map:
{:key1 "value1", :key2 "value2"}
;; Set: 
#{:foo :bar}
  • Listの区切りはコンマではなく空白
    • コンマも無視されて空白として解釈されるので使用はできる
  • consはList以外に使うとListにキャストされるっぽい ((cons 1 [])の返却値が(1)だった)
  • nthlastといったCollectionの先頭以外の要素を取得する関数はListとVectorの両方に使えるが先頭からのtraversalが必要なListよりもindexでアクセスできるVectorの方がパフォーマンスが良い
    • indexによる要素へのアクセスが必要な場合はVectorを使う
  • 全てのCollectionはimmutableでpersistent
    • persistent? -> “… these collections will do smart creation of new version of themselves by using structural sharing.”
  • conjはデータ構造によって要素を追加する場所が異なる
    • Listなら先頭、Vectorなら末尾
  • Mapでvalueを取得するときはgetを使う方法とkeyを関数として扱う方法がある
    • keyを関数とする方法がよりClojure的だが場所によってgetを使うこともある (明示のため)
  • Setに対する集合操作 (union, intersection, difference) を呼ぶ際は clojure.set/unionというように記述する

Binding

  • 変数に値をバインドする的な方法は (def foo "value")
    • 厳密にはいわゆる変数とは違うけどそれについてはChapter 3で
    • global variable的な立ち位置っぽい
  • letだとletの中だけアクセスできる
(def foo "foo")
foo
;; -> "foo"

(let [foo "foo2"
      bar "bar"]
    [foo bar])
;; -> ["foo2" "bar"]

foo
;; -> "foo"
bar
;; -> CompilerException java.lang.RuntimeException: Unable to resolve symbol: bar in this context...

Functions

  • Anonymous functionのfnがあって
(fn [<params>] <body>)
  • defn(def some-fn (fn [<params>] <body>))の略
(defn some-fn [<params>] <body>)
  • Anonymous functionは省略っぽい記述がある
    • #(<body including params>)
    • パラメータは% (複数ある際は%の後に数字をつける) で表す
;; Textの例
(#(str "Off we go" "!" " - " %1 %2) "again" "?")

Namespace

  • (ns foo.bar)で作成(と切り替え、はREPLのみ?)
  • *ns*がnamespaceを返す
  • require
    • 引数にnamespaceだけ: (require 'clojure.set)
      • Fully qualified namespaceでアクセスできる e.g. clojure.set/union
    • as: (require '[foo.bar :as fb])
      • (fb/some-fn) とかそんな
    • refer all: (require '[foo.bar :refer :all])
      • 呼び出したnamespaceに対象のnamespace内のすべてのsymbolをロードしてsymbolだけでアクセスできる
      • symbolの衝突とか由来が分かりにくかったりするのでやたらと使わない方が良い
  • userequire:refer :allしたのと同じ、だけどrequire:refer :allで書いた方が良いっぽい

Chapter 2. Flow and Functional Transformations

  • 用語 (本書を読むにあたって十分なレベルで)
    • expression: 評価されて結果が得られるコード
    • form: 評価してエラーが返ってこないexpression

Logic Tests and Flow Control

  • =はequalityを検査している
  • Equality of Collections
(= #{:a :b} #{:a :b})
;; => true
(= #{:a :b} [:a :b])
;; => false
(= #{:a :b} '(:a :b))
;; => false
(= '(:a :b) [:a :b])
;; => true
  • empty?seq

※追記 (2017/02/26) ※

Twitterで各collectionはSeqableを継承したIPersistentCollectionを実装しているという指摘をいただいて、Clojureソースコードを読み直してみると確かにそうだった。継承や実装をちゃんと見ていなかった。
しかしそれでも何か腑に落ちないものがあり、もう一度よく読み直してみたらそもそも自分が勘違いをしていたことがわかった。
つまり、最初私は各collectionが共通のinterfaceか何かを持っていて、「それの振る舞いとして」seq関数が実装されており、それを介してsequenceを得ることができると考えていた。
だが実際はseq関数 (の実装) とcollection及びそれらが持つ共通の抽象構造は分かれていて、seqcore.cljで定義された、これを通してcollectionをsequenceに変換する、構造とは独立した関数 (実際に処理が実装されているのはRT.java?) で、各collectionは単に遡っていくとSeqableを実装している構造体で、そのためにseqを通してsequenceにすることができる (RT.seqを見ると厳密にはこの説明は正しくないのだけど、簡単のため)、ということのようだ。

というかきちんとサンプルコード見れば(seq coll)となっているのだからseq関数自体は構造から独立してるのわかるじゃん…

※追記終わり※

  • someの返却値
    • 第二引数のcollectionの各要素に対してpredicateが最初に返すlogically trueな値
    • nil (predicateを満たす値がなかった場合)
  • someの第一引数 (predicate) にSetを使うこともある
    • logically falseな値をSetに入れると破綻する
  • condcase
    • マッチするものがない場合、condnilを、caseIllegalArgumentExceptionを返す
    • caseはdefaultを設定できる (リストの要素が奇数の場合最後のexpressionがdefaultとして評価される)

Functions Creating Functions ~

  • partial
    • curryingじゃなくてただの部分適用の話…?
  • 関数合成はcomp

Destructuring

  • :asで元々の構造を保持できる
  • :orでデフォルト値を設定できる
  • [{:keys [param1 param2]}]という形での指定が一般的

Recursion

  • looprecur
    • loopを用いると初期値の隠蔽ができる
    • recur再帰呼び出しに使われる (loopを使ってるとloopの開始地点から再帰処理を開始?)
  • recurはstackを積まないようにしてくれる
    • なんか条件がありそう (末尾再帰のみとか)

The Functional Shape of ~

  • mapreduce
  • 副作用が発生するタイミング
    • doall
  • mapには複数のcollectionを渡すことができる
    • それぞれの同じindexの要素を用いて関数を実行する
    • 素数が少ない方に合わせる
  • complement: 関数を引数にとり、反対の論理値を返す関数を返す
;; Vectorから`nil`を取り除く: 
(filter (complement nil?) [:a nil :b :c nil :d])
;; => (:a :b :c)
;; 以下も同じ挙動
(remove nil? [:a nil :b :c nil :d])
  • for
    • 複数のcollectionを与えるとネストする
    • :letによってfor内のlet bindingができる
    • :when <condition>を与えると条件を満たす時のみ評価が実行される
  • flatten
  • into
  • partition
    • partition-all
    • partition-by