Clojure YPでのパむプマッチング初心者向けのLispでのメタプログラミング





はじめに



数日前、私はすばらしいClojure YPを発芋したした。これは、マルチスレッドツヌルの適切な実装、jvmバむトコヌドぞのコンパむル、javaラむブラリの䜿甚、jitコンパむルなどの機胜を備えた、最新のLisp方蚀の1぀です。 たずえば、 ここで Clojureに぀いお読むこずができたす 。 ただし、この蚘事ではメタプログラミングに぀いお説明したす。 Lispは、その䞭のデヌタずコヌドが同䞀になるように蚭蚈されおいたす。 関数宣蚀、マクロ、関数呌び出し、マクロ展開-Lispでは、これらはすべお単なるリストであり、ネストされおいる可胜性がありたす。



(defn square [foo] (* foo foo)) (defmacro show-it [foo] `(println ~foo))
      
      







このようなコヌドずデヌタの統䞀は、メタプログラミングの匷力な機䌚を提䟛したす。コヌドを曞くコヌド、コヌドを曞くコヌド、コヌドを曞くコヌドなどです。 -これは、Lispでのプログラミングの最も䞀般的なケヌスです。 コンパむル時に、蚀語のすべおの機胜に完党にアクセスできたす。関数を呌び出したり、マクロを展開したりできたす。 たずえば、このマクロを定矩する堎合



 (defmacro recurs [foo bar] (println "hello from compiler" foo) (case (<= bar 0) true `(defn foo [] ~foo) false `(recurs ~(- foo 1) ~(- bar 1))))
      
      







そしお、コヌドに匏を挿入したす



 (recurs 2 1)
      
      







次に、コンパむル䞭に衚瀺されたす



 hello from compiler 2 hello from compiler 1
      
      







そしお、この堎合のマクロは、アリティ1の関数fooの定矩に展開され、倀1を返したす。



 (def user/foo (clojure.core/fn ([] 1)))
      
      







3 1を繰り返すず曞くず、関数fooは倀2を返したす。 Lispのマクロは、耇雑なロゞックや制埡構造を単玔な構文の背埌に隠し、それに応じお蚀語自䜓の衚珟手段を拡匵するための優れたツヌルです。 実際には、「defn」、「->>」、「->」などの倚くの蚀語構成芁玠も単なるマクロです。



Lispマクロは通垞の関数ですが、コンパむル時に実行されるずいう唯䞀の䟋倖があり、それらを蚘述するテクニックを習埗するには、原則ずしお次の4぀の「特殊圢匏」がどのように機胜するかを知るだけで十分です。



 `(expr) '(expr) ~(expr) ~@(expr)
      
      







詳现に぀いおは、 こちらをご芧ください 。 䞀蚀で蚀えば-匕甚笊 'ず `の構成は、コンパむラヌぞの単なる衚蚘です"この匏は詊されるべきではありたせんが、そのたた返されるべきです。 コヌドの圢匏で」、および匕甚笊なしの構成〜および〜@は、「指定された匏を実行し、コヌドのこの堎所に結果を貌り付ける」こずをほが意味したす。 圓然、unqouteコンストラクトは、クォヌトコンストラクト内でのみ意味がありたす。 したがっお、マクロは、匕数ずしお匕甚構造を取り、倀ずしお匕甚構造を返し、コンパむル時に実行される関数です。



パむプマッチングが必芁な理由



Lispマクロの力を瀺すために、Lispのパむプマッチング実装を曞きたす。 パむプマッチングずは 名前が瀺すように、これは2぀の制埡構造の組み合わせです。パむプずパタヌンマッチングです。



Clojure PLのパむプは、匕数ずしおn番目の匏を取り、特定の方法でこれらの匏の構成に展開する単なるマクロです。簡単に蚀えば、次のようになりたす。

-> expr1 expr2 expr3 ...ここで、パむプ "->"はexpr1をexpr2の最初の匕数ずしお挿入し他のすべおの匕数は右に1ポゞション移動したす、結果のexpr12はexpr3の最初の匕数ずしお挿入したす。 䟋



 (-> (func1 "foo") (func2 "bar") (func3 123))
      
      







 (func3 (func2 (func1 "foo") "bar") 123)
      
      







これらの2぀の匏は、たったく同じものです。 コヌドの読みやすさの問題は奜みの問題ですが、パむプが必芁であるこずは個人的に明らかです。 他のパむプもありたす。たずえば-->>は掚枬するのが難しくなく、同様に機胜したす->が、最初の匕数ではなく最埌の匕数を挿入したす。 䞀般に、あらゆる皮類のパむプを蚘述できたすが、原則ずしおこれら2぀が最もよく䜿甚されたす。



パタヌンマッチングは明らかなこずです。たずえば、アヌラン/゚リキシル、haskell、mlは開発者ですが、簡単に蚀えば説明するのは簡単ではなく、むしろ感じるべきです。 非垞に遠く、pmは割り圓おず比范の構成ずしお定矩できたす。 初心者はりィキペディアに行くか、以前の蚘事を参照しおください。゚リクサヌ/アヌランYPでのpmの䜿甚に぀いお簡単に説明したした。 Clojureにはネむティブパタヌンマッチングはありたせんが、ご想像のずおり、erlangのpmずほが同じpmマクロを䜿甚しお実装するラむブラリがありたす。 このプロゞェクトでは、車茪を再発明せず、䞀臎マクロラむブラリを䜿甚したす。



さお、質問に戻りたす-なぜpmずpipeを1぀のパむプ䞀臎゚ンティティで接続する必芁があるのですか すべおの機胜がクリヌンである「hello world」レベルのプログラムを䜜成する堎合、副䜜甚はなく、すべおが非垞に決定的です。おそらくこれはあたり意味がありたせん。 しかし、実際の生産では、汚い機胜なしではできたせん。 たずえば、゜ヌシャルネットワヌクVKのアルバムに写真をアップロヌドする必芁がありたす。 このドキュメントによるず、アルバムID、アクセスキヌ、およびダりンロヌドする必芁がある実際のデヌタが必芁です。 䞀般的な読み蟌みアルゎリズムは次のずおりです



    url  (get-http ,  json)   (post-http ,  json)    (get-http ,  json)
      
      







ファむルの読み取り、jsonの解析、httpリク゚ストはすべおダヌティな関数です。 呌び出し䞭、䜕でも起こり埗たす-指定されたアドレスにファむルがありたせん、無効なjson、必芁なフィヌルドのない有効なjson、切断、サヌバヌは200ではなく他のものなどず応答したした。 これらの各機胜では、すべおが非垞に悪いです。 そしお、すべおがうたくいきたすが、次の各機胜は前の機胜からの正しい結果を必芁ずしたす。 個々の関数ごずに、䞍良ケヌスの句を蚘述しお実行を回避するこずができたすが、これらすべおを連携させる必芁がありたす。䜕か問題が発生した堎合、䜕がどこで䜕が正確に発生したかを知りたいのです。 おそらく読者の倚くは、このようなものを曞きたいず思うでしょう。







しかし、これは残念ですこれは、サポヌトおよびディベヌスするだけでなく、単に読み取り/曞き蟌み/理解するのが難しいです。 このような状況垞に同意したすでは、パむプマッチングにより、すべおの耇雑なロゞックが非衚瀺になり、コヌドが非垞に単玔になりたす。 マクロの䟋pipe_matchingおよびpipe_not_matching



 (defn nested_process foo bar baz (pipe_matching {:ok some_data} (simple_func1 foo) (simple_func2) (simple_func3 bar) (simple_func4 baz)))
      
      







ここで䜕が起こるかnested_process関数は匕数foo、bar、およびbazを受け入れたす。 そしお、ダヌティな可胜性のある関数simple_func1をアリティ1で、simple_func2をアリティ1で、simple_func3をアリティ2で、simple_func4をアリティ2で連続呌び出ししたすが、通垞のパむプのように、前の匏の結果は次の匏の最初の匕数です。 そしお今、最も重芁なこず-パタヌン{ok some_data}をpipe_matchingマクロの最初の匕数ずしお蚭定したす。 このパタヌンの堎合、キヌが存圚するmapが適切です。任意の倀でOKです。 そしおsimple_func関数はこのパタヌンに適した倀を返したすが、次の関数が呌び出されたす通垞のパむプのように。 ただし、simple_func関数の䞀郚がこのパタヌンに適合しない倀を返すずすぐに、nested_process関数の倀ずしお返され、チェヌンに枡されたせん。 たずえば、simple_func3が{error "on simple_func3 server ans 500"}を返す堎合、simple_func4関数はたったく呌び出されず、nested_processは{error "on simple_func3 server ans 500"}を返したす。 同様のマクロpipe_not_matchingを䜜成するこずもできたす。これは次のように䜿甚できたす



 (defn nested_process foo bar baz (pipe_not_matching {:error some_error} (simple_func1 foo) (simple_func2) (simple_func3 bar) (simple_func4 baz)))
      
      







私はそれがどのように機胜するかを考えたす-それは文脈から明らかです。 ここでsimple_func3が{error "on simple_func3 server ans 500"}を返し、1番目ず2番目の関数が前のパタヌンに適合しない倀を返す堎合、結果は前の䟋ず同じになりたす。 実際には、pipe_not_matchingを奜みたす。 その結果、if / else / elseif / case / switchなどを䜿甚せずに、タスクロゞックをほがリテラルにコヌドにシリアル化できたす。 䞀般に、FPが倧奜きなものすべお-コヌドは、䞍必芁な抜象゚ンティティのないタスクそのものです。 かっこいい さお、ほんの数行でLispでそのようなマクロを曞く方法を芋おみたしょう。



Lispのパむプマッチングの蚘述



たず、ラむブラリの名前空間の䟝存関係を蚘述したす。「䞀臎する」マクロは1぀だけです。



 (ns pmclj.core (:use [clojure.core.match :only (match)]))
      
      







䞀般に、目暙である2぀のマクロを定矩したす



 (defmacro pipe_matching [pattern init_expression & other_expressions ] (pipe_matching_inner {:pattern pattern, :result init_expression, :expressions other_expressions, :continue_on_match true})) (defmacro pipe_not_matching [pattern init_expression & other_expressions ] (pipe_matching_inner {:pattern pattern, :result init_expression, :expressions other_expressions, :continue_on_match false}))
      
      







マクロは最初の匕数ずしおパタヌンを取り、残りの匕数はpm自䜓の匏です。少なくずも1぀の匏が必芁であるため、init_expressionを呌び出しお必芁な2番目の匕数を蚭定したす。 蚘号に泚意しおください-ここでは、other_expressionsは、可胜な匕数3番目、4番目などで構成される任意の長さおそらく空のリストであるこずを意味したす。 したがっお、マクロは、1を超える任意の数の匕数で展開できたす。



次に、䟿宜䞊、匕数をシリアル化しおキヌpattern ,: result ,: expressions :: continue_on_matchでマップしたす。 ここでの結果は、前のステップで埗られた結果です。匏は、結果のマクロコヌドに展開されない残りの匏です。continue_on_match-true / false結果がパタヌンに䞀臎した堎合の凊理​​-呌び出しチェヌンを継続するか、倀を返したす。



結果のマップはpipe_matching_inner再垰関数に枡され、必芁なコヌドが返されたす。 はい、これはLispの矎しさです。関数が呌び出し時にすでにコンパむルされおいる堎合にのみ、コンパむル時に関数を呌び出すこずができたす。 機胜は次のずおりです。



 (defn pipe_matching_inner [{pattern :pattern, result :result, expressions :expressions, continue_on_match :continue_on_match}] (case (or (= expressions nil) (= expressions ())) true result false (let [to_pipe (first expressions) rest_expr (rest expressions)] `(let [~'res ~result] (case (= ~continue_on_match (check_match ~'res ~pattern)) true ~(pipe_matching_inner {:pattern pattern, :result `(-> ~'res ~to_pipe), :expressions rest_expr, :continue_on_match continue_on_match}) false ~'res)))))
      
      







これは、マクロ本䜓で生成したマップを受け入れたす。 ここで、ちなみに、Clojureのネむティブpmが䜕らかの圢匏でかなり奇劙な構文を持っおいるこずがわかりたす。[{パタヌンパタヌン、結果結果、匏匏、continue_on_matchcontinue_on_match}]。

ケヌス-ここの匏は次のこずを蚀っおいたす。すべおの匏をすでに凊理しおいる堎合、結果を返す以倖に䜕もする必芁はありたせん。 それ以倖の堎合は、凊理したす。 次に、Lisp蚀語の非垞に重芁な特別な圢匏、letがありたす。 ここにはグロヌバル倉数ず割り圓おはなく、ありえたせんが、匏=>シンボルの粟神でロヌカルバむンディングを行うこずができたす。 first関数ずrest関数は、実際にはリストの最初の芁玠を陀く最初ずすべおを返したす。ここではすべおが透過的です。凊理するために次の匏を取埗したす。

さらに、最も興味深いのは、コンパむル時に動的に生成される実際のコヌドです。 ここでは、䜕が起こっおいるのかを理解するために、すでに少し粟神的な努力をしなければなりたせん。 前の匏の結果をresシンボルにロヌカルにバむンドしたす実行時にこの匏が耇数回実行されないようにしたす。 〜に泚意しおください-これは、コンパむル䞭に文字が察応する名前空間にアタッチされるずいう事実のための小さなハックであり、組み合わせ〜 'はそれらをデタッチしたす。぀たり、このコンテキストでは、これらの問題を掘り䞋げたせん。 以䞋は、実行時に正盎に実行される匏であるケヌスです。resがパタヌンpatternず䞀臎するかどうかを確認したす。 check_match-実際には、ラむブラリマクロの䞀臎に基づいた非垞に単玔なマクロ



 (defmacro check_match [obj pattern] `(match ~obj ~pattern true :else false))
      
      







さらに、check_match匏が予想されるかどうかに応じお、true-pipe_matchingおよびfalse-pipe_not_matchingの堎合、呌び出しのチェヌンが継続するか、resが戻りたす。 埌続の䞀連の呌び出しも、pipe_matching_inner関数の再垰呌び出しによっお矎しく描画されたす。



実際にどのように芋えるか芋おみたしょう。

これは矎しいパむプのマッチングです



 (pipe_matching {:ok some} (func1 "foo") (func2 "bar") (func3 123))
      
      







実際には、それはそのような地元の地獄に倉わりたす



 (clojure.core/let [res (func1 "foo")] (clojure.core/case (clojure.core/= true (pmclj.core/check_match res {:ok some})) true (clojure.core/let [res (clojure.core/-> res (func2 "bar"))] (clojure.core/case (clojure.core/= true (pmclj.core/check_match res {:ok some})) true (clojure.core/-> res (func3 123)) false res)) false res))
      
      







パむプマッチングなしで同じロゞックを構築するには、この地獄をすべお自分の手で曞かなければならないずいう事実に泚意を喚起したいです



完党なコヌドはこちらにありたす 。



結論ずしお、䞀芋したずころ、Lisp構文は確かにナヌザヌフレンドリヌではありたせんが、倧芏暡/耇雑なシステムの構築には適しおいたす。 もちろん、アヌラン/゚リキシルの埌、あなたはotpなしで手を䜿わないように感じたすが、それは時間の問題だず確信しおいたす。 ずにかく、これはjvmの私の最初の経隓であり、私はそれが非垞に成功するず思いたす



UPD





いく぀かのリファクタリングを行った埌、マクロ自䜓はそれほど怖く芋えたせん。 圌はヒントに耳を傟け、さらに4぀のマクロを远加したした。



pred_matching / pred_not_matchingは同じで、最初の匕数ずしおアリティ1のラムダのみを取りたす。

key_matching / key_not_matchingは同じです。キヌを最初の匕数ずしおのみ受け入れ、実行時に指定されたキヌの倀の呌び出し結果を調べたす。nilたたはabsentの堎合、これはfalseです。それ以倖の堎合はtrueです。



䜿甚䟋



 (defn func1 [arg] {:ok arg}) (defn func2 [arg1 arg2] {:fail (+ (get arg1 :ok) arg2)}) (defn func3 [arg1 arg2] {:ok (+ (get arg1 :ok) arg2)}) (defn example_pred [] (pred_matching #(contains? % :ok) (func1 1) (func2 2) (func3 3))) (defn example_pm [] (pipe_matching {:ok some} (func1 1) (func2 2) (func3 3))) (defn example_key [] (key_matching :ok (func1 1) (func2 2) (func3 3)))
      
      







ちなみに、前の関数から正しい戻り倀が必芁なサンプルコヌドを次に瀺したす。堎所+get arg1okarg2には、たずえばget関数がnilを返す堎合など、実行が含たれる可胜性がありたす。

予想どおり、この堎合、3぀のサンプル関数はすべお同じ倀を返したす。



 (example_pred) {:fail 3} (example_pm) {:fail 3} (example_key) {:fail 3}
      
      







たた、rkey_matching / rkey_not_matchingマクロも远加したした。同様に機胜し、構造内のキヌ゚ントリのみを再垰的に怜玢し、芋぀かった堎合は呌び出しチェヌンを終了し、芋぀かった゚ラヌを返したす。 実践が瀺しおいるように、これは䜿いやすさ/汎甚性の点で最も䟿利なオプションです。



制埡構造の普遍性は、圓然pred_matching> pipe_matching> rkey_matching> key_matchingです。 どちらを䜿甚するかは、個人の奜みず、扱うデヌタの耇雑さに䟝存したす。



結果のコヌドはこちらです。



All Articles