Clojureの destructuring (分配束縛)の書き方。
set
などのマイナーな記法もできるだけ網羅。
サンプルコードはlet
だが、関数の引数など分配束縛が有効なフォームではどこでも同じ。
用語
-
destructuring
の訳としてここでは分配束縛を使います。 -
sequential
: Clojureのシーケンス抽象可能な構造全般に対してこう呼びます。 -
associative
: mapのような構造全般に対してこう呼びます。一般的にはassociative array
を連想配列と訳すようです。
参考
- Clojure - Destructuring in Clojure
- GPソフト Wiki - ClojureのDestructuring(分配束縛)
- Clojure Destructuring Tutorial and Cheat Sheet
- The complete guide to Clojure destructuring.
1. 基本
(1) Sequential Destructuring シーケンシャルなデータ構造に対する分配束縛
vectorやlistなどのsequentialな構造に対しては、vectorを使って分配を表現します。
; vector
(let [[a b c d] [10 20 30 40]]
(println a b c d))
;=> 10 20 30 40
; list
(let [[a b c d] '(10 20 30 40)]
(println a b c d))
;=> 10 20 30 40
文字列にも使用可能。
; string
(let [[a b c d] "ABCD"]
(prn a b c d) ;=> \A \B \C \D
(println (class a))) ;=> java.lang.Character
変数名が重複すると後勝ち
(let [[a b c c] [10 20 30 40]]
(println c))
;=> 40
余った要素は単に無視される
(let [[a b] [10 20 30 40]]
(println a b))
;=> 10 20
該当する要素がないとnil
(let [[a b c d] [10 20 30]]
(println d))
;=> nil
ネストした構造も可
(let [[a [b1 b2] c] [10 [21 22] 30]]
(println b1 b2))
;=> 21 22
(let [[a b c] [10 [21 22] 30]]
(println b))
;=> [21 22]
setはsequentialではない(順序がない)のでエラー 1
(let [[a b c d] #{10 20 30 40}]
(println a b c))
;=> UnsupportedOperationException nth not supported on this type: PersistentHashSet clojure.lang.RT.nthFrom (RT.java:947)
不要な要素をスキップする_
1番目と3番目の要素は要らなくて、2番めと4番目の要素だけ欲しい
(let [[_ b _ d] [10 20 30 40]]
(println b d))
;=> 20 40
ちなみに、_
に何か特別な機能があるわけではないです。_
も単なる変数です。2
使用しない変数名に_
を使う慣例があるだけで、値は_
にきちんと束縛されています。
(let [[_ b _ d] [10 20 30 40]]
(println _))
;=> 30
元の構造全てを束縛する:as
(let [[a b c d :as all] [10 20 30 40]]
(println all))
;=> [10 20 30 40]
残り全てを束縛する&
(let [[a & rest] [10 20 30 40]]
(println rest))
;=> (20 30 40)
&
は、残りが1つしかなくてももちろんシーケンスで返る
(let [[a b c & rest] [10 20 30 40]]
(println rest))
;=> (40)
&
は、残りが1つもないとnil
が返る。(空のシーケンス ()
ではない。)
(let [[a b c d & rest] [10 20 30 40]]
(println rest))
;=> nil
ちなみに、:as
は元のデータ型そのままで束縛されるのに対し、
&
は元のシーケンスの型と同じになるとは限りません。
上記のコードで言えば、
:as
で束縛されるのは、元のvectorと同じPersistentVector
で、
&
で束縛されるのはPersistentVector$ChunkedSeq
です。
(let [[& rest :as all] [10 20 30 40]]
(println rest (class rest)) ;=> (10 20 30 40) clojure.lang.PersistentVector$ChunkedSeq
(println all (class all))) ;=> [10 20 30 40] clojure.lang.PersistentVector
文字列だと
(let [[& rest :as all] "ABCDEF"]
(println rest (class rest)) ;=> (A B C D E F) clojure.lang.StringSeq
(println all (class all))) ;=> ABCDEF java.lang.String
全部使ってみる
:as
や&
は、ネストした構造のどのレベルでも使用可能
(let [[a [b1 & b_rest :as b_all] _ _ & rest :as all] [10 '(21 22 23) 30 40 50]]
(println a)
(println b1)
(println b_rest)
(println b_all)
(println rest)
(println all))
;=> 10
;=> 21
;=> (22 23)
;=> (21 22 23)
;=> (50)
;=> [10 (21 22 23) 30 40 50]
(2) Associative destructuring アソシエーティブなデータ構造に対する分配束縛
associativeな構造に対しては、mapを使って分配を表現します。
; Keyword key
(let [{a :a b :b} {:a 10 :b 20}]
(println a b))
;=> 10 20
; String key
(let [{a "a" b "b"} {"a" 10 "b" 20}]
(println a b))
;=> 10 20
; Symbol key
(let [{a 'a b 'b} {'a 10 'b 20}]
(println a b))
;=> 10 20
キーと同名の変数に値を束縛する:keys
, :strs
, :syms
を使えば、もっと簡潔に書くことができます。
-
:keys
: キーが、キーワードのとき(Keywords
) -
:strs
: キーが、文字列のとき(Strings
) -
:syms
: キーが、シンボルのとき(Symbols
)
これらを使って、上記のコードを書き直すと次のようになります。 3
; Keyword key
(let [{:keys [a b]} {:a 10 :b 20}]
(println a b))
;=> 10 20
; String key
(let [{:strs [a b]} {"a" 10 "b" 20}]
(println a b))
;=> 10 20
; Symbol key
(let [{:syms [a b]} {'a 10 'b 20}]
(println a b))
;=> 10 20
ネストした構造も可
(let [{a :a {b1 :b1 b2 :b2} :b c :c} {:a 10 :b {:b1 21 :b2 22} :c 30}]
(println a b1 b2 c))
;=> 10 21 22 30
該当のキーが存在しないとnil
(let [{a :a b :b c :c} {:a 10 :b 20}]
(println a b c))
;=> 10 20 nil
(let [{:keys [a b c]} {:a 10 :b 20}]
(println a b c))
;=> 10 20 nil
元の構造全てを束縛する:as
(let [{:keys [a b c] :as all} {:a 10 :b 20}]
(println all))
;=> {:a 10, :b 20}
該当のキーが存在しない場合のデフォルト値を指定する:or
(let [{:keys [a b c] :or {a 100 b 200 c 300}} {:a 10 :b 20}]
(println a b c))
;=> 10 20 300
自分は、:or
に渡すmapのキーをキーワードで書いてしまう間違いをしてしまったことが何度かあります。
こんな↓バグです。
(let [{:keys [a b c] :or {:a 100 :b 200 :c 300}} {:a 10 :b 20}]
(println a b c))
;=> 10 20 nil
- alephの作者の人もミスってたので、結構みんなよくやるミスなのかもしれないです。
Fix :or map - keys are bound symbols, not lookup keys · ztellman/aleph@7d6f2f5
2. 応用
(1) setの分配束縛
setの分配束縛は、キーとバリューが同一なmapのように扱います。
; Keyword
(let [{:keys [spring summer autumn winter] :or {winter :none} :as all} #{:spring :summer :fall}]
(prn spring summer autumn winter) ;=> :spring :summer nil :none
(prn all)) ;=> #{:fall :spring :summer}
; String
(let [{:strs [spring summer autumn winter] :or {winter :none} :as all} #{"spring" "summer" "fall"}]
(prn spring summer autumn winter) ;=> "spring" "summer" nil :none
(prn all)) ;=> #{"summer" "fall" "spring"}
; Symbol
(let [{:syms [spring summer autumn winter] :or {winter :none} :as all} #{'spring 'summer 'fall}]
(prn spring summer autumn winter) ;=> spring summer nil :none
(prn all)) ;=> #{summer fall spring}
イメージ(※あくまで挙動のイメージで、実装を表しているわけではありません)。
#{:spring :summer :fall}
; ↓
{:spring :spring, :summer :summer, :fall :fall}
この記法は、関数の引数で一連のフラグを受け取るような際に便利だと、こちらには書かれています。
(2) mapを使ってシーケンスを分配束縛
実はvectorではなくmapで、シーケンスを分配束縛することもできます。
(a) シーケンスのインデックスをキーとしてmapで束縛
シーケンスのインデックス(0,1,2,3,...)がassociativeな構造のキーとして扱われます。
(let [{a 0 d 3 e 4} [10 20 30 40]]
(println a d e))
;=> 10 40 nil
(let [{c 2 :as all} "ABCDE"]
(prn c) ;=> \C
(prn all)) ;=> "ABCDE"
イメージ(※あくまで挙動のイメージで、実装を表しているわけではありません)。
"ABCDE"
; ↓
(\A \B \C \D \E)
; ↓
{0 \A, 1 \B, 2 \C, 3 \D, 4 \E}
; ^^^^^
; c に束縛
この記法を使っているのは、あまり見たことがありません。
(b) associativeな構造のシーケンスを&
を使ってmapで束縛
associativeな構造のシーケンスは、&
を使えば、それを活かして分配束縛できます。
associativeな構造のシーケンスとは、要素数が偶数で奇数番目の要素がキー、偶数番目の要素がバリューにそれぞれ相当するような構造のシーケンスのことです。
; associative
(let [[& {a :a b :b c :c}] [:a 10 :b 20]]
(println a b c))
;=> 10 20 nil
:keys
, :strs
, :syms
を使うことも可能。
(let [[& {:keys [a b c]}] [:a 10 :b 20]]
(println a b c))
そもそも、対象のデータ型全体がassociativeなlistなら&
なんか使用しなくてもいけたりします。
vectorは駄目です。なぜだろう・・・
; associativeに分配束縛できる
(let [{:keys [a b]} '(:a 10 :b 20)]
(println a b))
;=> 10 20
; listではなくvectorなので、associativeに分配束縛はできない
(let [{:keys [a b]} [:a 10 :b 20]]
(println a b))
;=> nil nil
もちろん&
は、「残り全て」を扱います。
(let [[m _ & {a :a b :b c :c}] [{:foo "bar"} "baz" :a 10 :b 20]]
(println m) ;=> {:foo bar}
(println a b c)) ;=> 10 20 nil
(let [[m _ & {:keys [a b c]}] [{:foo "bar"} "baz" :a 10 :b 20]]
(println m) ;=> {:foo bar}
(println a b c)) ;=> 10 20 nil
ちなみに、対象のシーケンスが奇数だとエラーになります。
(let [[& {:keys [a b c]}] [:a 10 :b]])
;=> IllegalArgumentException No value supplied for key: :b clojure.lang.PersistentHashMap.create (PersistentHashMap.java:77)
この記法は、関数の引数でオプションをmapで受け取るような時に結構使うんじゃないかと思います。
(defn write-wonderfully [name & {:keys [new append encoding quietly]}]
(prn new append encoding quietly)
(when verbose (comment ....))
; ....
)
(write-wonderfully "foo.txt" :encoding "utf-8" :new true)
Rubyでも似たようなシグネチャはよく見かけます。
def write_wonderfully(name, *opts)
p opts
p Hash[*opts]
end
write_wonderfully("foo.txt", :encoding, "utf-8", :new, true)
(3) 複雑な構造
ネストした複雑な構造も分配束縛が可能です。
(def members
[{:name "john"
:birthday (-> "yyyy-MM-dd" java.text.SimpleDateFormat. (.parse "1999-10-01"))
:hobbies [:running :reading :travel]}
{:name "pete"
:birthday (-> "yyyy-MM-dd" java.text.SimpleDateFormat. (.parse "1997-04-03"))
:hobbies [:reading]}
{:name "mark"
:birthday (-> "yyyy-MM-dd" java.text.SimpleDateFormat. (.parse "2001-12-30"))
:hobbies [:shopping :programming :travel :swimming]}])
(let [[{[initial :as whole_name] :name hobbies :hobbies} _ {:keys [name birthday]}] members
fmt (java.text.SimpleDateFormat. "yyyy年MM月dd日(EE)")]
(println "1人目のメンバーのイニシャルは" initial "です。")
(println "1人目のメンバーの名前は" whole_name "です。")
(println "1人目のメンバーの名前は" (clojure.string/join " と " hobbies) "です。")
(println "3人目のメンバーの名前は" name "で、誕生日は" (.format fmt birthday) "です。"))
;=> 1人目のメンバーのイニシャルは j です。
;=> 1人目のメンバーの名前は john です。
;=> 1人目のメンバーの名前は :running と :reading と :travel です。
;=> 3人目のメンバーの名前は mark で、誕生日は 2001年12月30日(日) です。
もっと良い例をください。