ClojureのDestructuring(分配束縛)

前置き

Clojureでは、変数をバインドするときにdestructuring(分配束縛)が使えます。destructuringを使ったコードは、最初のうちは暗号みたいに感じるのですが、慣れてくるとコードの意図がより伝わりやすくなります(たぶん)。

destructuringは、de + structure + ingなので、「構造を分解する」みたいなニュアンスでしょう。構造を分解してバインド(束縛)するので、「分配束縛」ってことですね。

そもそものアイディアは…

  • バインドとは、シンボル(≒変数)に、値を対応付ける(≒代入する)ことである
  • つまり、シンボル ← 値
  • 例えば、msg ← "hello"
  • 左辺にシンボルを含んだ構造を置き、右辺に同等の構造を持った値を置いてもいいじゃないか?
  • [num msg key] ← [100 "hello" :foo]

ということみたいです。

まずはサンプルコードで

destructuringを使わない場合
(let [lis '(1 2 3)]
  (println (first lis))           ; 1
  (println (nth lis 2)))          ; 3
destructuringを使った場合
(let [[a b c] '(1 2 3)]
  (println a)           ; 1
  (println c))          ; 3

どちらも、リストの1番目と3番目の要素をprintlnするコードです。destructuringを使ったコードでは、「リスト」という構造を分解して、aとbとcにバインドしました。letの[]の中の、[a b c]がバインドの左辺で、'(1 2 3)が右辺です。

発動する場面

destructuringが発動するのは、変数をバインドする場面です。例えば…

  • 関数を呼ぶときの、関数の仮引数への実引数のバインド
  • letやloop特殊フォームでの、ローカル変数への値のバインド
  • bindingマクロでの、ダイナミック変数への値のバインド

Clojureはバインドを表現するときにベクタを使うので、ベクタを引数に取るオペレータにはdestructuringを使えるかもしれない、と考えても良いのかもしれません。defnやfnで関数定義するときは、引数リストをベクタで指定しますし、letやloop、bindingでもバインドをベクタで書きますよね。

もっとサンプル

関数呼び出しにおけるdestructuringの例を挙げます。letの例では、バインドの左辺と右辺が隣り合っていましたが、関数呼び出しの場合は離れ離れになります(左辺は関数定義の中で、右辺は関数呼び出しの中)。

destructuringなし
(defn foo
  "リストとマップと文字列を引数に取る関数。"
  [lis m s]
  (println (second lis))
  (println (m :a))
  (println s))

(foo '(1 2 3) {:a 100 :b 101} "hello") ; 2
                                       ; 100
                                       ; hello
destructuringあり
(defn foo
  "同じく、リストとマップと文字列を引数に取る関数。"
  [[a b] {v :a} s]                     ; ←ここがdestructuringのコード
  (println b)
  (println v)
  (println s))

(foo '(1 2 3) {:a 100 :b 101} "hello") ; 2
                                       ; 100
                                       ; hello

fooの定義の中の[a b]と{v :a}がバインドの左辺で、foo呼び出しの'(1 2 3)と{:a 100 :b 101}が右辺です。

リストを分解する場合とマップを分解する場合とで、書き方が異なることに注意して下さい。リストを分解する場合は[]、マップを分解する場合は{}を使います。

上記の例では、第1仮引数に[a b]と書くことにより、リストの1番目と2番目の要素がaとbにバインドされます。3番目以降の要素は無視されます。もしリストが要素を1つしか持ってなければ、bの値はnilになります。この例では、let本体部でaを使ってないので、[a b]の代わりに[_ b]と書き、「1番目の要素は使わない」ということを暗示するスタイルが一般的のようです(文法的にはaも_も同格です)。例えば3番目の要素だけが欲しいなら、[_ _ c]とでも書けば良いでしょう。

fooの第2仮引数の{v :a}は、マップから :a というキーに対応する値を取り出し、それをvにバインドする、という意味です。:aに対応する値が無い場合は、vの値はnilになります。

基本ルール

  • destructuringが使えるのは、sequentialな構造と、associativeな構造
  • sequentialな構造とは、リスト、ベクタ、シーケンス、文字列、Java配列など、nth関数が使えるもの
  • associativeな構造とは、マップ、ベクタ、文字列、Java配列など、キーと値の対応を持つもの
  • ベクタ、文字列、Java配列をassociativeな構造として見るときは、インデックスがキーになる
  • sequentialな構造をdestructuringするときは、バインドの左辺を[]で書く
  • associativeな構造をdestructuringするときは、バインドの左辺を{}で書く

[]と{}は、自由にネスト&混在させることができます。

(let [[{[_ {n :name}] :children}]
      [{:name "Tom" :age 25 :children [{:name "Becky"} {:name "Linda"}]}]]
  (println n))          ; Linda

読みやすいとは言えませんが…。

ちょっと進んだ使い方

&で残り全部にバインド
(let [[a b c & d] [1 2 3 4 5 6]]
  (println d))          ; (4 5 6)
:asで全体にバインド
(let [[a b :as all] [1 2 3]]
  (println a)           ; 1
  (println b)           ; 2
  (println all))        ; [1 2 3]

(let [{a 0 b 1 :as all} ["hello" "hi" "hey"]]
  (println a)           ; hello
  (println b)           ; hi
  (println all))        ; [hello hi hey]
文字列をdestructuring
(let [[_ _ c] "001"]
  c)                    ; \1

(let [{c 2} "001"]
  c)                    ; \1
:orでデフォルト値を指定
(let [{a :a z :z :or {a 1 z 26}} {:a 100}]
  (println a)           ; 100
  (println z))          ; 26
マップのキーがキーワードで、キーと同名の変数にバインドしたいなら、:keys
(let [{:keys [name age]}
      {:name "Tom" :age 25 :children [{:name "Becky"} {:name "Linda"}]}]
  (println name)        ; Tom
  (println age))        ; 25
マップのキーがシンボルで、キーと同名の変数にバインドしたいなら、:syms
(let [{:syms [name age]}
      {'name "Tom" 'age 25 'children [{'name "Becky"} {'name "Linda"}]}]
  (println name)        ; Tom
  (println age))        ; 25
マップのキーが文字列で、キーと同名の変数にバインドしたいなら、:strs
(let [{:strs [name age]}
      {"name" "Tom" "age" 25 "children" [{"name" "Becky"} {"name" "Linda"}]}]
  (println name)        ; Tom
  (println age))        ; 25

さらに進んだ使い方

関数の可変長引数を、destructuringで受ける
(defn foo [& [a b c]]
  (println [a b c]))

(defn bar [& {:keys [a b c]}]
  (println [a b c]))

(foo 1 2 3 4)                ; [1 2 3]
(bar :a 1 :b 2 :c 3 :d 4)    ; [1 2 3]
(foo :a 1 :b 2 :c 3 :d 4)    ; [:a 1 :b]

普通、& の後ろの仮引数には、余った実引数のシーケンスがバインドされますが、barのようにassociative構造に対するdestructuringで受けている場合は、自動的にマップ化された後でバインドされるんですね。

(defn buz [& {:keys [a b c] :as all}]
  (class all))

(buz :a 1 :b 2 :c 3 :d 4)    ; clojure.lang.PersistentHashMap
Last modified:2013/10/19 15:23:37
Keyword(s):
References:[Clojureでモナド(Part2)] [FP: 関数型プログラミング]
This page is frozen.