Clojure

Clojureにおけるデストラクチャリング

デストラクチャリングとは何か?

デストラクチャリングは、データ構造内の値に簡潔に名前をバインドする方法です。デストラクチャリングにより、より簡潔で読みやすいコードを作成できます。

ベクトル内の値を抽出して名前を付ける例を考えてみましょう。

(def my-line [[5 10] [10 20]])

(let [p1 (first my-line)
      p2 (second my-line)
      x1 (first p1)
      y1 (second p1)
      x2 (first p2)
      y2 (second p2)]
  (println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"

これは完全に有効ですが、ベクトル内の値を抽出して名前を付けるコードは、私たちの意図を不明瞭にします。デストラクチャリングにより、複雑なデータ構造の重要な部分をより簡潔に抽出して名前を付けることができ、コードをよりクリーンにすることができます。

;= Using the same vector as above
(let [[p1 p2] my-line
      [x1 y1] p1
      [x2 y2] p2]
 (println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"

各変数を明示的にバインドするのではなく、シーケンシャルな順序に基づいてバインディングを記述します。「バインディングを記述する」というかなり奇妙な表現ですが、もう一度見てみましょう。

[[5 10] [10 20]]のように見えるmy-lineというデータ構造があります。デストラクチャリングフォームでは、それぞれがベクトルである2つの要素p1p2を含むベクトルを作成します。これにより、ベクトル[5 10]がシンボルp1に、ベクトル[10 20]がシンボルp2にバインドされます。構造体自体ではなくp1p2の要素を操作したいので、同じletステートメント内でp1p2をデストラクチャリングします。ベクトルp1[5 10]のように見えるため、デストラクチャリングするには、2つの要素x1y1を含むベクトルを作成します。これにより、5がシンボルx1に、10がシンボルy1にバインドされます。p2についても同様で、10x2に、20y2にバインドされます。この時点で、データの操作に必要なものがすべて揃いました。

シーケンシャルデストラクチャリング

Clojureのデストラクチャリングは、シーケンシャルデストラクチャリングとアソシアティブデストラクチャリングの2つのカテゴリに分けられます。シーケンシャルデストラクチャリングは、シーケンシャルなデータ構造をletバインディング内のClojureベクトルとして表します。

このタイプのデストラクチャリングは、リスト、ベクトル、seqs、文字列、配列、そしてnthをサポートするあらゆる種類の線形時間でトラバースできるデータ構造に使用できます。

(def my-vector [1 2 3])
(def my-list '(1 2 3))
(def my-string "abc")

;= It should come as no surprise that this will print out 1 2 3
(let [[x y z] my-vector]
  (println x y z))
;= 1 2 3

;= We can also use a similar technique to destructure a list
(let [[x y z] my-list]
  (println x y z))
;= 1 2 3

;= For strings, the elements are destructured by character.
(let [[x y z] my-string]
  (println x y z)
  (map type [x y z]))
;= a b c
;= (java.lang.Character java.lang.Character java.lang.Character)

シーケンシャルデストラクチャリングの鍵は、値をベクトル内のシンボルに1対1でバインドすることです。たとえば、ベクトル[x y z]は、リスト'(1 2 3)と各要素を1対1で照合します。

場合によっては、デストラクチャリングするコレクションのサイズが、デストラクチャリングバインディングのサイズと完全に一致しないことがあります。ベクトルのサイズが小さすぎる場合、余分なシンボルはnilにバインドされます。

(def small-list '(1 2 3))
(let [[a b c d e f g] small-list]
  (println a b c d e f g))
;= 1 2 3 nil nil nil nil

一方、コレクションが大きすぎる場合、余分な値は単に無視されます。

(def large-list '(1 2 3 4 5 6 7 8 9 10))
(let [[a b c] large-list]
  (println a b c))
;= 1 2 3

デストラクチャリングにより、バインドする(またはしない)要素と、それらをバインドする方法を完全に制御できます。

多くの場合、コレクションのすべての要素ではなく、特定の要素のみへのアクセスが必要です。

(def names ["Michael" "Amber" "Aaron" "Nick" "Earl" "Joe"])

最初の要素を1行に、残りを別の行に印刷したいとします。

(let [[item1 item2 item3 item4 item5 item6] names]
  (println item1)
  (println item2 item3 item4 item5 item6))
;= Michael
;= Amber Aaron Nick Earl Joe

このバインディングは機能しますが、デストラクチャリングを使用してもかなりぎこちないです。代わりに、&を使用して、末尾の要素をシーケンスに結合できます。

(let [[item1 & remaining] names]
  (println item1)
  (apply println remaining))
;= Michael
;= Amber Aaron Nick Earl Joe

使用しないバインディングは、任意のシンボルにバインドすることで無視できます。

(let [[item1 _ item3 _ item5 _] names]
  (println "Odd names:" item1 item3 item5))
;= Odd names: Michael Aaron Earl

このための慣例は、上記のようにアンダースコアを使用することです。

:as allを使用して、ベクトル全体をシンボルallにバインドできます。

(let [[item1 :as all] names]
  (println "The first name from" all "is" item1))
;= The first name from [Michael Amber Aaron Nick Earl Joe] is Michael

少し休憩して、:as&のタイプをもう少し詳しく見てみましょう。

(def numbers [1 2 3 4 5])
(let [[x & remaining :as all] numbers]
  (apply prn [remaining all]))
;= (2 3 4 5) [1 2 3 4 5]

ここで、remainingnumbersベクトルの残りの要素を含むシーケンスにバインドされ、allは元のvectorにバインドされています。文字列をデストラクチャリングするとどうなるでしょうか?

(def word "Clojure")
(let [[x & remaining :as all] word]
  (apply prn [x remaining all]))
;= \C (\l \o \j \u \r \e) "Clojure"

ここでは、allは元の構造(文字列、ベクトル、リストなど)にバインドされ、xは文字\Cにバインドされ、remainingは残りの文字のリストです。

これらのテクニックを任意に組み合わせて使用できます。

(def fruits ["apple" "orange" "strawberry" "peach" "pear" "lemon"])
(let [[item1 _ item3 & remaining :as all-fruits] fruits]
  (println "The first and third fruits are" item1 "and" item3)
  (println "These were taken from" all-fruits)
  (println "The fruits after them are" remaining))
;= The first and third fruits are apple and strawberry
;= These were taken from [apple orange strawberry peach pear lemon]
;= The fruits after them are (peach pear lemon)

デストラクチャリングはネストすることもでき、任意のレベルのシーケンシャルな構造にアクセスできます。最初のベクトルmy-lineに戻りましょう。

(def my-line [[5 10] [10 20]])

このベクトルはネストされたベクトルで構成されており、直接アクセスできます。

(let [[[x1 y1][x2 y2]] my-line]
  (println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"

ネストされたベクトルがある場合、任意のレベルで:asまたは&を使用することもできます。

(let [[[a b :as group1] [c d :as group2]] my-line]
  (println a b group1)
  (println c d group2))
;= 5 10 [5 10]
;= 10 20 [10 20]

アソシアティブデストラクチャリング

アソシアティブデストラクチャリングはシーケンシャルデストラクチャリングに似ていますが、代わりにアソシアティブ(キーと値)構造(マップ、レコード、ベクトルなど)に適用されます。アソシアティブバインディングは、キーでマップの値を簡潔に抽出することに関係します。

まず、デストラクチャリングなしでマップから値を抽出する例を考えてみましょう。

(def client {:name "Super Co."
             :location "Philadelphia"
             :description "The worldwide leader in plastic tableware."})

(let [name (:name client)
      location (:location client)
      description (:description client)]
  (println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.

letバインディングの各行は基本的に同じです。キーの名前でマップから値を抽出し、同じ名前のローカルにバインドします。

アソシアティブデストラクチャリングを使用して同じことを行う最初の例を以下に示します。

(let [{name :name
       location :location
       description :description} client]
  (println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.

デストラクチャリングフォームは、ベクトルではなくマップになり、letの左側にあるシンボルではなく、マップがあります。マップのキーは、letでバインドするシンボルです。デストラクチャリングマップの値は、アソシアティブ値で検索するキーです。ここではキーワード(最も一般的なケース)ですが、数値、文字列、シンボルなど、任意のキー値にすることができます。

シーケンシャルデストラクチャリングと同様に、マップに存在しないキーをバインドしようとすると、バインド値はnilになります。

(let [{category :category} client]
  (println category))
;= nil

ただし、アソシアティブデストラクチャリングでは、:orキーを使用して、アソシアティブ値にキーが存在しない場合にデフォルト値を指定することもできます。

(let [{category :category, :or {category "Category not found"}} client]
  (println category))
;= Category not found

:orの値は、バインドされたシンボル(ここではcategory)が式"Category not found"にバインドされるマップです。clientにcategoryが見つからない場合、代わりに:orマップで見つかり、その値にバインドされます。

シーケンシャルデストラクチャリングでは、通常、不要な値を_でバインドします。アソシアティブデストラクチャリングでは、構造全体をトラバースする必要がないため、使用しないキーをデストラクチャリングフォームから単に省略できます。

マップ全体にアクセスする必要がある場合は、シーケンシャルデストラクチャリングと同様に、:asキーを使用して、受信する値全体をバインドできます。

(let [{name :name :as all} client]
  (println "The name from" all "is" name))
;= The name from {:name Super Co., :location Philadelphia, :description The world wide leader in plastic table-ware.} is Super Co.

:as:orキーワードは、単一のデストラクチャリングで組み合わせることができます。

(def my-map {:a "A" :b "B" :c 3 :d 4})
(let [{a :a, x :x, :or {x "Not found!"}, :as all} my-map]
  (println "I got" a "from" all)
  (println "Where is x?" x))
;= I got A from {:a "A" :b "B" :c 3 :d 4}
;= Where is x? Not found!

元の例には、アソシアティブデストラクチャリングフォームに冗長な情報(ローカルバインディング名とキー名)が含まれていることに気づいたかもしれません。:keysキーを使用して、重複をさらに削除できます。

(let [{:keys [name location description]} client]
  (println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.

この例は前のバージョンとまったく同じです。name(:name client)に、location(:location client)に、description(:description client)にバインドします。

:keysキーはキーワードキーを持つアソシアティブ値用ですが、文字列キーとシンボルキーにはそれぞれ:strs:symsもあります。これらのすべての場合において、ベクトルにはローカルバインディング名であるシンボルが含まれています。

(def string-keys {"first-name" "Joe" "last-name" "Smith"})

(let [{:strs [first-name last-name]} string-keys]
  (println first-name last-name))
;= Joe Smith

(def symbol-keys {'first-name "Jane" 'last-name "Doe"})

(let [{:syms [first-name last-name]} symbol-keys]
  (println first-name last-name))
;= Jane Doe

アソシアティブデストラクチャリングはネストでき、必要に応じてシーケンシャルデストラクチャリングと組み合わせることができます。

(def multiplayer-game-state
  {:joe {:class "Ranger"
         :weapon "Longbow"
         :score 100}
   :jane {:class "Knight"
          :weapon "Greatsword"
          :score 140}
   :ryan {:class "Wizard"
          :weapon "Mystic Staff"
          :score 150}})

(let [{{:keys [class weapon]} :joe} multiplayer-game-state]
  (println "Joe is a" class "wielding a" weapon))
;= Joe is a Ranger wielding a Longbow

キーワード引数

特別なケースの1つは、キーワード引数の解析にアソシアティブデストラクチャリングを使用することです。オプション:debug:verboseを受け取る関数について考えてみましょう。これらはオプションマップで指定できます。

(defn configure [val options]
  (let [{:keys [debug verbose] :or {debug false, verbose false}} options]
    (println "val =" val " debug =" debug " verbose =" verbose)))

(configure 12 {:debug true})
;;val = 12  debug = true  verbose = false

ただし、これらのオプション引数を次のように追加の「キーワード」引数として渡すことができれば、より簡単に記述できます。

(configure 12 :debug true)

この呼び出しスタイルをサポートするために、アソシアティブデストラクチャリングは、キーワード引数の解析のために、キーと値のペアのリストまたはシーケンスでも機能します。シーケンスは可変長関数のrest引数から取得されますが、シーケンシャルデストラクチャリングではなく、アソシアティブデストラクチャリングを使用してデストラクチャリングされます(マップのキーと値のペアであるかのようにデストラクチャリングされます)。

(defn configure [val & {:keys [debug verbose]
                        :or {debug false, verbose false}}]
  (println "val =" val " debug =" debug " verbose =" verbose))

(configure 10)
;;val = 10  debug = false  verbose = false

(configure 5 :debug true)
;;val = 5  debug = true  verbose = false

;; Note that any order is ok for the kwargs
 (configure 12 :verbose true :debug true)
;;val = 12  debug = true  verbose = true

キーワード引数の使用は、長年にわたりClojureコミュニティで流行したり廃れたりしてきました。現在、それらは主に、REPLまたはAPIの最上位レイヤーで人が入力することが期待されるインターフェースを提示する場合に使用されます。一般的に、コードの内側のレイヤーでは、オプションを明示的なマップとして渡す方が簡単でした。ただし、Clojure 1.11では、キーワード引数を期待する関数に、交互のキー→値、またはそれらの同じマッピングのマップ、さらにはその前にキー→値を持つマップを渡す機能が追加されました。したがって、上記のconfigureへの呼び出しは、上記で示したものに加えて、次のいずれかの形式をとることができます。

 (configure 12 {:verbose true :debug true})
;;val = 12  debug = true  verbose = true

 (configure 12 :debug true {:verbose true})
;;val = 12  debug = true  verbose = true

キーワード引数を期待する関数への後続のマップは、キー→値のペアとして提供されるデフォルトキーを上書きする場合に役立つことがよくあります。

名前空間付きキーワード

マップのキーが名前空間付きキーワードの場合、ローカルバインディングシンボルは名前空間を持つことが許可されていませんが、それを使用してデストラクチャリングすることもできます。名前空間付きキーをデストラクチャリングすると、キーのローカル名部分に値がバインドされ、名前空間は削除されます。(したがって、名前空間のないキーと同様に:orを使用できます。)

(def human {:person/name "Franklin"
            :person/age 25
            :hobby/hobbies "running"})
(let [{:keys [hobby/hobbies]
       :person/keys [name age]
       :or {age 0}} human]
  (println name "is" age "and likes" hobbies))
;= Franklin is 25 and likes running

:keysのみを使用した名前空間キーワードのデストラクチャリングは、ローカルバインディングの衝突を引き起こす可能性があります。すべてのマップデストラクチャリングオプションを組み合わせることができるため、任意のローカルバインディングフォームを個別に定義できます。

(def human {:person/name "Franklin"
            :person/age 25
            :hobby/name "running"})
(let [{:person/keys [age]
       hobby-name :hobby/name
       person-name :person/name} human]
  (println person-name "is" age "and likes" hobby-name))
;= Franklin is 25 and likes running

自動解決されたキーワードを使用したデストラクチャリングも可能です。この場合も、キーの名前部分のみにバインドされます。

;; this assumes you have a person.clj namespace in your project
;; if not do the following at your repl instead: (create-ns 'person) (alias 'p 'person)
(require '[person :as p])

(let [person {::p/name "Franklin", ::p/age 25}
      {:keys [::p/name ::p/age]} person]
  (println name "is" age))

;= Franklin is 25

自動解決されたキーワードを使用したマップの作成とデストラクチャリングにより、現在の名前空間のrequireで定義された名前空間エイリアス(ここではp)を使用してコードを記述できます。これにより、コードの一箇所で変更できる名前空間の非直接参照手段が得られます。

デストラクチャリングのコンテキストでバインドされたすべてのシンボルは、さらにデストラクチャリングできます。これにより、シーケンシャルおよびアソシエイティブなデストラクチャリングの両方に対して、ネストされた方法でデストラクチャリングを使用できます。あまり明白ではありませんが、これは&の後に定義されたシンボルにも適用されます。

この例では、&シーケンスをその場でデストラクチャリングして、残りの引数をオプションとしてデコードしています(したがって、2つの引数をシーケンシャルに、残りをアソシエイティブにデストラクチャリングしていることに注意してください)。

(defn f-with-options
  [a b & {:keys [opt1]}]
  (println "Got" a b opt1))

(f-with-options 1 2 :opt1 true)
;= Got 1 2 true

デストラクチャリングの場所

明示的または暗黙的なletバインディングがある場所であれば、どこでもデストラクチャリングを使用できます。

デストラクチャリングが最もよく見られる場所の1つは、関数に渡された引数を分解することです。

ここでは、標準的なlet x = this、let y = thatなど…を使用しています。これも完全に有効なコードですが、冗長です。

(defn print-coordinates-1 [point]
  (let [x (first point)
        y (second point)
        z (last point)]
    (println "x:" x ", y:" y ", z:" z)))

firstsecondnth、またはgetを使用してデータ構造を分解しているコードが見られる場合は、デストラクチャリングで簡素化できる可能性があります。letを書き直すことから始められます。

(defn print-coordinates-2 [point]
  (let [[x y z] point]
    (println "x:" x ", y:" y ", z:" z)))

Clojureで関数を定義する場合、letと同様に、入力パラメータにデストラクチャリングを適用できます。

(defn print-coordinates-3 [[x y z]]
  (println "x:" x ", y:" y ", z:" z))

入力されたポイントデータを分解する複数のコード行を、そのデータの構造に関する簡潔な記述に置き換え、データをローカル値にバインドしました。

より現実的な例として、悪名高いジョン・スミスの基本的な連絡先情報を含むマップを作成しましょう。

(def john-smith {:f-name "John"
                 :l-name "Smith"
                 :phone "555-555-5555"
                 :company "Functional Industries"
                 :title "Sith Lord of Git"})

ジョンの個人情報が得られたので、このマップ内の値にアクセスする必要があります。

(defn print-contact-info [{:keys [f-name l-name phone company title]}]
  (println f-name l-name "is the" title "at" company)
  (println "You can reach him at" phone))

(print-contact-info john-smith)
;= John Smith is the Sith Lord of Git at Functional Industries
;= You can reach him at 555-555-5555

この関数は、:keysショートカットを使用して入力のアソシエイティブなデストラクチャリングを行い、提供された連絡先情報を印刷します。

しかし、ジョンに素敵な手紙を送りたい場合はどうでしょうか?

(def john-smith {:f-name "John"
                 :l-name "Smith"
                 :phone "555-555-5555"
                 :address {:street "452 Lisp Ln."
                           :city "Macroville"
                           :state "Kentucky"
                           :zip "81321"}
                 :hobbies ["running" "hiking" "basketball"]
                 :company "Functional Industries"
                 :title "Sith Lord of Git"})

住所がありますが、これを実現するために元の構造にマップをネストする必要がありました。

(defn print-contact-info
  [{:keys [f-name l-name phone company title]
    {:keys [street city state zip]} :address
    [fav-hobby second-hobby] :hobbies}]
  (println f-name l-name "is the" title "at" company)
  (println "You can reach him at" phone)
  (println "He lives at" street city state zip)
  (println "Maybe you can write to him about" fav-hobby "or" second-hobby))

(print-contact-info john-smith)
;= John Smith is the Sith Lord of Git at Functional Industries
;= You can reach him at 555-555-5555
;= He lives at 452 Lisp Ln. Macroville Kentucky 81321
;= Maybe you can write to him about running or hiking

マクロ

マクロ作成者は、デストラクチャリングを組み込んだマクロを作成する必要がある場合があります。最も一般的な方法は、既にデストラクチャリングを実行するもの(letloopfnなど)への呼び出しを生成することです。clojure.coreにおけるこの例としては、if-letwhen-letwhen-someなどがあります。

ただし、まれに、マクロ内でデストラクチャリングを自分で解決したい場合があります。この場合は、(ドキュメント化されていない)clojure.core/destructure関数を使用します。これはデストラクチャリングロジックを実装しており、letloopが実際に呼び出すものです。destructure関数はマクロ内で呼び出されるように設計されており、フォームを受け取ってフォームを返すことを期待しています。

(destructure '[[x & remaining :as all] numbers])
;= [vec__1 numbers
;=  x (clojure.core/nth vec__1 0 nil)
;=  remaining (clojure.core/nthnext vec__1 1)
;=  all vec__1]

結果は、より明確にするためにここでフォーマットされています。この例は、デストラクチャリングが内部的にどのように機能するかについても理解を深めるのに役立つはずです。

原文著者: Michael Zavarella