私はclojureプロトコルとそれらが解決すべき問題を理解しようとしています。 clojureプロトコルの内容と理由を明確に説明している人はいますか?
Clojureのプロトコルの目的は、効率的な方法で式の問題を解決することです。
だから、表現の問題は何ですか?それは拡張性の基本的な問題を指します:私たちのプログラムは操作を使用してデータ型を操作します。プログラムが進化するにつれて、新しいデータ型と新しい操作でプログラムを拡張する必要があります。特に、既存のデータ型で機能する新しい操作を追加できるようにし、既存の操作で機能する新しいデータ型を追加します。 Andこれを真にしたいextension、つまりexistingプログラムを変更したくない、既存の抽象化を尊重したい、拡張機能は、別個のモジュール、別個のネームスペース、別個にコンパイル、別個にデプロイ、別個に型チェックされるようにする必要があります。それらをタイプセーフにしたいのです。 [注:これらのすべてがすべての言語で意味をなすわけではありません。しかし、たとえば、Clojureのような言語でも、タイプセーフにするという目標は理にかなっています。 check type-safetyが静的にできないからといって、コードをランダムに壊したいわけではありませんよね?]
表現の問題は、どのように実際に言語でそのような拡張性を提供するのですか?
手続き型および/または関数型プログラミングの典型的な単純な実装では、新しい操作(手順、関数)を追加するのは非常に簡単ですが、基本的に操作はいくつかを使用してデータ型で機能するため、新しいデータ型を追加することは非常に難しいことがわかります大文字と小文字の区別(switch
、case
、パターンマッチング)の場合、新しいケースを追加する必要があります。つまり、既存のコードを変更します。
func print(node):
case node of:
AddOperator => print(node.left) + '+' + print(node.right)
NotOperator => '!' + print(node)
func eval(node):
case node of:
AddOperator => eval(node.left) + eval(node.right)
NotOperator => !eval(node)
ここで、タイプチェックなどの新しい操作を追加する場合は簡単ですが、新しいノードタイプを追加する場合は、すべての操作で既存のパターンマッチング式をすべて変更する必要があります。
そして、典型的な素朴なオブジェクト指向では、正反対の問題があります:既存の操作で動作する新しいデータ型を追加するのは簡単です(継承またはオーバーライドすることにより)既存のクラス/オブジェクト。
class AddOperator(left: Node, right: Node) < Node:
meth print:
left.print + '+' + right.print
meth eval
left.eval + right.eval
class NotOperator(expr: Node) < Node:
meth print:
'!' + expr.print
meth eval
!expr.eval
ここでは、必要なすべての操作を継承、オーバーライド、または実装するため、新しいノードタイプの追加は簡単ですが、すべてのリーフクラスまたは基本クラスのいずれかに追加する必要があるため、新しい操作の追加は難しく、既存のコード。
式問題を解決するためのいくつかの言語には、Haskellには型クラスがあり、Scalaには暗黙の引数があり、RacketにはUnits、GoにはInterfaces、CLOSおよびClojureにはMultimethodsがあります。 attemptそれを解決しますが、何らかの方法で失敗します:C#とJavaのインターフェイスと拡張メソッド、RubyのMonkeypatching、Python、ECMAScript。
Clojureは、実際にはすでにhas式の問題を解決するためのメカニズム:Multimethodsであることに注意してください。 EPでOOにある問題は、操作と型を一緒にバンドルすることです。マルチメソッドでは、それらは分離されます。FPにある問題は、繰り返しますが、マルチメソッドでは、これらは別々です。
それでは、プロトコルとマルチメソッドを比較してみましょう。両方とも同じことをするからです。または、別の言い方をすれば、すでにhave Multimethodsの場合、なぜプロトコルなのでしょうか?
プロトコルがマルチメソッドを介して提供する主なものはグループ化です。複数の機能をグループ化し、「これら3つの機能一緒にフォームプロトコルFoo
」と言うことができます。 Multimethodsでそれを行うことはできません。それらは常に独立しています。たとえば、Stack
プロトコルがboth、Push
およびpop
関数togetherで構成されることを宣言できます。
それでは、なぜMultimethodsをグループ化する機能を追加しないのですか?純粋に実用的な理由があり、それが私が導入文で「効率的」という言葉を使用した理由です:パフォーマンス。
Clojureはホスト言語です。つまりanother言語のプラットフォーム上で実行されるように特別に設計されています。そして、Clojureを実行したいプラットフォーム(JVM、CLI、ECMAScript、Objective-C)のほとんどすべてが、最初のタイプのsolelyをディスパッチするための特別な高性能サポートを持っていることがわかりました。引数。 Clojure Multimethods OTOHは、-任意のプロパティまたはすべての引数でディスパッチします。
そのため、プロトコルでは、onlyをfirst引数に、onlyをそのタイプに(またはnil
の特殊なケースとして)ディスパッチするように制限されています。
これは、プロトコル自体の考え方に対する制限ではなく、基盤となるプラットフォームのパフォーマンス最適化にアクセスするための実用的な選択です。特に、プロトコルはJVM/CLIインターフェースに簡単にマッピングできるため、非常に高速になります。実際、Clojureの現在のJavaまたはClojure自体のC#で記述されている部分を書き換えることができるほど高速です。
Clojureは、バージョン1.0以降、実際にプロトコルをすでに持っています。たとえば、Seq
はプロトコルです。しかし、1.2までは、Clojureでプロトコルを書くことができず、ホスト言語でプロトコルを書く必要がありました。
プロトコルをJavaなどのオブジェクト指向言語の「インターフェース」に概念的に類似していると考えるのが最も役立ちます。プロトコルは、特定のオブジェクトに対して具体的な方法で実装できる抽象関数セットを定義します。
例:
(defprotocol my-protocol
(foo [x]))
1つのパラメーター「x」に作用する「foo」という1つの関数を持つプロトコルを定義します。
その後、プロトコルを実装するデータ構造を作成できます。
(defrecord constant-foo [value]
my-protocol
(foo [x] value))
(def a (constant-foo. 7))
(foo a)
=> 7
ここでは、プロトコルを実装するオブジェクトが最初のパラメーターx
として渡されることに注意してください。これは、オブジェクト指向言語の暗黙的な「this」パラメーターに似ています。
プロトコルの非常に強力で便利な機能の1つは、プロトコルをオブジェクトに拡張できることですオブジェクトが元々プロトコルをサポートするように設計されていない場合でもです。例えば必要に応じて、上記のプロトコルをJava.lang.Stringクラスに拡張できます。
(extend-protocol my-protocol
Java.lang.String
(foo [x] (.length x)))
(foo "Hello")
=> 5