私はclojureプロトコルとそれらが解決することになっているどんな問題を理解しようとしています。誰もがクロジュールプロトコルの何をそしてなぜの明確な説明を持っていますか?
私はclojureプロトコルとそれらが解決することになっているどんな問題を理解しようとしています。誰もがクロジュールプロトコルの何をそしてなぜの明確な説明を持っていますか?
回答:
Clojureのプロトコルの目的は、効率的な方法で式の問題を解決することです。
それで、表現の問題は何ですか?これは拡張性の基本的な問題を指します。プログラムは操作を使用してデータ型を操作します。プログラムが進化するにつれて、新しいデータ型と新しい操作でプログラムを拡張する必要があります。特に、既存のデータ型で機能する新しい操作を追加できるようにしたいし、既存の操作で機能する新しいデータ型を追加したい。そして、これを真の拡張にしたい、つまり、変更したくない既存のものプログラム、既存の抽象化を尊重したい、拡張を個別のモジュール、個別の名前空間、個別にコンパイル、個別にデプロイ、個別に型チェックしたい。私たちはそれらをタイプセーフにしたいと考えています。[注:これらのすべてがすべての言語で意味をなすわけではありません。しかし、たとえば、それらをタイプセーフにするという目標は、Clojureのような言語でも意味があります。静的にチェックできないから型安全性をいって、コードをランダムに壊したいというわけではありませんよね。]
式の問題は、実際に言語でそのような拡張性をどのように提供するのですか?
手続き型プログラミングや関数型プログラミングの典型的な素朴な実装の場合、新しい操作(プロシージャ、関数)を追加するのは非常に簡単ですが、新しいデータ型を追加するのは非常に困難です。ソートケース差別(の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)
これで、新しい操作、たとえば型チェックを追加する場合は簡単ですが、新しいノードタイプを追加する場合は、すべての操作で既存のすべてのパターンマッチング式を変更する必要があります。
そして、典型的なナイーブOOの場合、正反対の問題があります。既存の操作と連動する新しいデータ型を追加することは簡単です(継承または上書きすることにより)が、基本的に変更を意味するため、新しい操作を追加することは困難です。既存のクラス/オブジェクト。
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には単位があり、Goにはインターフェースがあり、CLOSとClojureにはマルチメソッドがあります。試みる「解決策」もありますそれを解決とするが、何らかの方法で失敗する。C#とJavaのインターフェイスと拡張メソッド、RubyのMonkeypatching、Python、ECMAScript。
Clojureには、実際には式の問題を解決するためのメカニズムが既にあることに注意してください:マルチメソッド。OOがEPで抱えている問題は、OOが操作とタイプをバンドルしていることです。マルチメソッドでは、それらは独立しています。FPの問題は、操作とケースの区別が一緒にバンドルされていることです。繰り返しますが、マルチメソッドでは、それらは分離されています。
では、プロトコルとマルチメソッドを比較してみましょう。どちらも同じことを行うからです。それとも、別の言い方をするには:なぜプロトコルを我々がすでにあれば持っているマルチメソッドを?
プロトコルがマルチメソッドを介して提供する主なものはグループ化です。複数の関数をグループ化して、「これら3つの関数が一緒にプロトコルを形成する」と言うことができますFoo
。マルチメソッドを使用してそれを行うことはできません。それらは常に独立しています。たとえば、あなたがいることを宣言することができStack
プロトコルで構成され、両方の A push
とpop
関数一緒に。
では、マルチメソッドをグループ化する機能を追加しないのはなぜですか?純粋に実用的な理由があり、それが私が紹介文で「効率的」という言葉を使用した理由です:パフォーマンス。
Clojureはホストされた言語です。つまり、別の言語のプラットフォーム上で実行するように特別に設計されています。そして、Clojureを実行したいほとんどすべてのプラットフォーム(JVM、CLI、ECMAScript、Objective-C)は、最初の引数のタイプのみでディスパッチするための特殊な高性能サポートを備えていることがわかります。ClojureマルチメソッドOTOHは、すべての引数の任意のプロパティをディスパッチします。
だから、プロトコルは発送にはあなたを制限するだけで最初の引数とだけそのタイプ(または上の特殊なケースとしての上nil
)。
これはプロトコル自体の概念に対する制限ではなく、基盤となるプラットフォームのパフォーマンス最適化にアクセスするための実用的な選択です。特に、プロトコルにはJVM / CLIインターフェースへの簡単なマッピングがあり、非常に高速になります。実際には、Clojure自体のJavaまたはC#で現在記述されているClojureの部分を書き換えることができるほど高速です。
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
this
Clojureコードでも。