Edited at

Clojureの分配束縛まとめ

More than 3 years have passed since last update.

便利だけどしょっちゅう忘れてしまうClojureのdestructuringの書き方をまとめました。

setの分配束縛とかマイナーな記法もできるだけ書いたつもりです。

サンプルコードはletで書いていますが、関数の引数など分配束縛が有効なフォームではどこでも同じように書けます。


用語



  • destructuringの訳としてここでは分配束縛を使います。


  • sequential: Clojureのシーケンス抽象可能な構造全般に対してこう呼びます。


  • associative: mapのような構造全般に対してこう呼びます。一般的にはassociative arrayを連想配列と訳すようです。


参考


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を使えば、もっと簡潔に書くことができます。



  1. :keys: キーが、キーワードのとき(Keywords


  2. :strs: キーが、文字列のとき(Strings


  3. :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


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日(日) です。

あまりいい例が書けませんでした。。





  1. mapで分配束縛する方法はあります。後述。 



  2. IDEやeditorによっては、__から始まる変数名を、使用する意図のない変数として解釈してくれるものがあるようです。例えば、IntelliJ IDEA (Cursive) では、_の変数が未使用でも警告が表示されません。 



  3. 簡潔になった感があまりないですが、束縛する変数が多くなったり、構造がネストしていたりして複雑になると恩恵が大きくなります。