Clojure

スレッディングマクロガイド

スレッディングマクロ(アローマクロとも呼ばれます)は、ネストされた関数呼び出しを線形の流れに変換し、可読性を向上させます。

thread-first マクロ (->)

慣用的なClojureでは、純粋関数は不変のデータ構造を目的の出力形式に変換します。マップに2つの変換を適用する関数を考えてみましょう。

(defn transform [person]
   (update (assoc person :hair-color :gray) :age inc))

(transform {:name "Socrates", :age 39})
;; => {:name "Socrates", :age 40, :hair-color :gray}

transform は一般的なパターンの例です。値を受け取り、複数の変換を適用し、パイプラインの各ステップは前のステップの結果を入力として受け取ります。多くの場合、thread-first マクロ -> を使用して書き換えることで、このタイプのコードを改善できます。

(defn transform* [person]
   (-> person
      (assoc :hair-color :gray)
      (update :age inc)))

初期値を最初の引数として受け取り、-> は、それを1つ以上の式にスレッドします。

注:このコンテキストでの「スレッド」(値を関数のパイプラインに渡すことを意味する)という言葉は、並行実行スレッドの概念とは無関係です。

2番目のフォームから始めて、マクロは最初の値を最初の引数として挿入します。これは、後続の各ステップで繰り返され、前の計算の結果が次のフォームの最初の引数として挿入されます。2つの引数を持つ関数呼び出しのように見えるものは、実際には3つの引数を持つ呼び出しです。スレッド化された値は関数名の直後に挿入されるためです。説明のために、挿入ポイントを3つのコンマでマークすると役立つ場合があります。

(defn transform* [person]
   (-> person
      (assoc ,,, :hair-color :gray)
      (update ,,, :age inc)))

実際にはあまり見られませんが、Clojureではコンマは空白であるため、この視覚的な補助は有効なClojure構文です。

意味的には、transform*transform と同等です。アローマクロはコンパイル時に元のコードに展開されます。いずれの場合も、関数の戻り値は最後の計算の結果、つまり update の呼び出しです。書き換えられた関数は、変換の説明のように読めます。「人を連れてきて、白髪にし、年齢を増やし、結果を返す」。もちろん、不変の値のコンテキストでは、実際の変更は行われません。代わりに、関数は更新された属性を持つ新しい値を返すだけです。

構文的には、スレッディングマクロを使用すると、リーダーは最も内側の式から外側に向かって読むのではなく、適用順に左から右に読むことができます。

thread-last (->>) および thread-as (as->) マクロ

-> マクロは、純粋に構文的な変換規則に従います。各式について、スレッド化された値を関数名と最初の引数の間に挿入します。スレッディング式は (f arg1 arg2 …​) 形式の関数呼び出しであることに注意してください。括弧のないベアシンボルまたはキーワードは、単一の引数を持つ単純な関数呼び出しとして解釈されます。これにより、単項関数の簡潔なチェーンが可能になります。

(-> person :hair-color name clojure.string/upper-case)

;; equivalent to

(-> person (:hair-color) (name) (clojure.string/upper-case))

ただし、スレッド化された引数を最初の位置に挿入するとは限らないため、-> は普遍的に適用できるわけではありません。10未満のすべての正の奇数の整数の2乗の合計を計算する関数を考えてみましょう。

(defn calculate []
   (reduce + (map #(* % %) (filter odd? (range 10)))))

transform と同様に、calculate は変換のパイプラインですが、前者とは異なり、スレッド化された値は引数リストの最後の位置にある各関数呼び出しに表示されます。thread-first マクロの代わりに、thread-last マクロ ->> を使用する必要があります。

(defn calculate* []
   (->> (range 10)
        (filter odd? ,,,)
        (map #(* % %) ,,,)
        (reduce + ,,,)))

繰り返しますが、通常は省略されますが、3つのコンマは引数が挿入される場所を示しています。ご覧のとおり、->> を使用してスレッド化されたフォームでは、スレッド化された値は引数リストの先頭ではなく最後に挿入されます。

Thread-firstとthread-lastは、状況によって使い分けられます。どちらが適切かは、変換関数のシグネチャによって異なります。最終的には、使用されている関数のドキュメントを参照する必要がありますが、いくつかの経験則があります。

  • 慣例により、シーケンスを操作するコア関数は、シーケンスを最後の引数として想定しています。したがって、mapfilterremovereduceinto などを含むパイプラインは、通常 ->> マクロを呼び出します。

  • 一方、データ構造を操作するコア関数は、操作する値を最初の引数として想定しています。これらには、assocupdatedissocget、およびそれらの -in バリアントが含まれます。これらの関数を使用してマップを変換するパイプラインでは、多くの場合 -> マクロが必要です。

  • Java相互運用を介してメソッドを呼び出す場合、Javaオブジェクトは最初の引数として渡されます。このような場合、たとえば、文字列にプレフィックスがあるかどうかを確認するために、-> が役立ちます。

    (-> a-string clojure.string/lower-case (.startsWith "prefix"))

    より特殊な相互運用マクロ..dotoにも注意してください。

最後に、->->> も適用できない場合があります。パイプラインは、さまざまな挿入ポイントを持つ関数呼び出しで構成される場合があります。これらの場合、より柔軟な代替手段である as-> を使用する必要があります。 as-> は、2つの固定引数と可変数の式を想定しています。 -> と同様に、最初の引数は後続のフォームにスレッドされる値です。2番目の引数はバインディングの名前です。後続の各フォームでは、バインドされた名前を前の式の結果に使用できます。これにより、値を最初または最後だけでなく、任意の引数の位置にスレッドできます。

(as-> [:foo :bar] v
  (map name v)
  (first v)
  (.substring v 1))

;; => "oo"

some->、some->>、および cond->

Clojureのより特殊なスレッディングマクロの2つである some->some->> は、Javaメソッドとインターフェースする場合に最も一般的に使用されます。 some-> は、値をいくつかの式にスレッドするという点で -> に似ています。ただし、チェーン内の任意の時点で式が nil と評価された場合、実行を短絡します。Java相互運用のコンテキストにおけるアローマクロの一般的な問題の1つは、Javaメソッドは nilnull)を渡されることを想定していないことです。これらの場合に NullPointerException を回避する1つの方法は、明示的なガードを追加することです。

(when-let [counter (:counter a-map)]
  (inc (Long/parseLong counter)))

some-> は、同じ効果をより簡潔に実現します。

(some-> a-map :counter Long/parseLong inc)

a-map にキー :counter がない場合、式全体は例外を発生させるのではなく nil と評価されます。実際、この動作は非常に便利であるため、スレッディングが必要ない場合でも some-> が使用されるのが一般的です。

(some-> (compute) Long/parseLong)

;; equivalent to

(when-let [a-str (compute)]
  (Long/parseLong a-str))

-> と同様に、マクロ cond-> は初期値を取りますが、前者とは異なり、引数リストを test, expr ペアのシーケンスとして解釈します。 cond-> は値を式にスレッドしますが、テストに失敗した式はスキップします。各ペアについて、test が評価されます。結果がtruthyの場合、式はスレッド化された値を最初の引数として挿入して評価されます。そうでない場合、評価は次の test, expr ペアに進みます。親戚とは異なり、some-> または condcond-> は、テストが false または nil と評価された場合でも、評価を短絡することはありません。

(defn describe-number [n]
  (cond-> []
    (odd? n) (conj "odd")
    (even? n) (conj "even")
    (zero? n) (conj "zero")
    (pos? n) (conj "positive")))

(describe-number 3) ;; => ["odd" "positive"]
(describe-number 4) ;; => ["even" "positive"]

cond->> は、スレッド化された値を各フォームの最後の引数として挿入しますが、それ以外の場合は同様に機能します。

元の作成者:Paulus Esterhazy