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)
だった)nth
やlast
といった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
を使うこともある (明示のため)
- keyを関数とする方法がよりClojure的だが場所によって
- 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
- Fully qualified namespaceでアクセスできる e.g.
- as:
(require '[foo.bar :as fb])
(fb/some-fn)
とかそんな
- refer all:
(require '[foo.bar :refer :all])
- 呼び出したnamespaceに対象のnamespace内のすべてのsymbolをロードしてsymbolだけでアクセスできる
- symbolの衝突とか由来が分かりにくかったりするのでやたらと使わない方が良い
- 引数にnamespaceだけ:
use
がrequire
に: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
living clojureのseqの話読んでてよくわからなかったからclojureのソース読んでみてるんだけどseqの云々よりインデントが時々ずれてるのに出くわしてあばばってなった
— nash (@nashcft) February 21, 2017
empty?におけるseqの話は、collectionが空かどうか検査するのにseq関数が使われてて、実装上seqは引数のcollectionが要素を持たないとnilを、要素を持ってたらsequenceを返すので、clojureの評価では空が判定できるよ、という話か
— nash (@nashcft) February 21, 2017
なんか各collectionの下かどこかにsequenceという抽象構造があって共通の振る舞いが云々という話かと思った
— nash (@nashcft) February 21, 2017
※追記 (2017/02/26) ※
Twitterで各collectionはSeqable
を継承したIPersistentCollection
を実装しているという指摘をいただいて、Clojureのソースコードを読み直してみると確かにそうだった。継承や実装をちゃんと見ていなかった。
しかしそれでも何か腑に落ちないものがあり、もう一度よく読み直してみたらそもそも自分が勘違いをしていたことがわかった。
つまり、最初私は各collectionが共通のinterfaceか何かを持っていて、「それの振る舞いとして」seq
関数が実装されており、それを介してsequenceを得ることができると考えていた。
だが実際はseq
関数 (の実装) とcollection及びそれらが持つ共通の抽象構造は分かれていて、seq
はcore.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に入れると破綻する
cond
とcase
- マッチするものがない場合、
cond
はnil
を、case
はIllegalArgumentException
を返す case
はdefaultを設定できる (リストの要素が奇数の場合最後のexpressionがdefaultとして評価される)
- マッチするものがない場合、
Functions Creating Functions ~
partial
- curryingじゃなくてただの部分適用の話…?
- 関数合成は
comp
Destructuring
:as
で元々の構造を保持できる:or
でデフォルト値を設定できる[{:keys [param1 param2]}]
という形での指定が一般的
Recursion
loop
とrecur
recur
はstackを積まないようにしてくれる- なんか条件がありそう (末尾再帰のみとか)
The Functional Shape of ~
map
とreduce
- 副作用が発生するタイミング
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