1. ホーム
  2. clojure

[解決済み] clojureプロトコルの簡単な説明

2022-05-12 18:20:24

質問

私はclojureプロトコルとそれが解決することになっている問題を理解しようとしています。どなたか、clojureプロトコルのWhatsとWhysについて明確な説明を持っている方はいらっしゃいますか?

どのように解決するのですか?

Clojureにおけるプロトコルの目的は、Expression Problemを効率的に解決することです。

では、式の問題とは何でしょうか?それは、拡張性の基本的な問題を指します。私たちのプログラムは、操作を使ってデータ型を操作します。プログラムが進化するにつれて、新しいデータ型や新しい操作でプログラムを拡張する必要があります。特に、既存のデータ型で動作する新しい操作を追加できるようにしたいし、既存の操作で動作する新しいデータ型も追加したい。 はこれを真としたい 拡張機能 を変更したくないのです。 既存の 拡張は別のモジュールで、別の名前空間にあり、別々にコンパイルされ、別々に配置され、別々に型チェックされることを望んでいます。タイプセーフであってほしいのです。[注:これらのすべてがすべての言語で意味をなすわけではありません。しかし、例えば、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、Python、ECMAScript のモンキーパッチなどです。

Clojureは実際にすでに を持っています。 を持っています。OOがEPで抱えている問題は、操作と型を一緒に束ねてしまうことです。マルチメソッドでは、それらは別々である。FPが抱える問題は、操作と場合分けを一緒にしてしまうことである。繰り返しになりますが、Multimethodsではそれらは別個のものなのです。

では、ProtocolとMultimethodsを比較してみましょう。どちらも同じことをするのですから。別の言い方をすれば なぜプロトコルなのかというと、すでに を持っているのに、なぜプロトコルを使うのか? があるのに、なぜプロトコルなのか?

プロトコルがマルチメソッドより優れている点は、グループ化です。 を一緒に プロトコルを形成する Foo "。マルチメソッドではこのようなことはできません。マルチメソッドは常にそれ自体で成り立っています。例えば Stack プロトコルの構成 ともに a pushpop 機能 共に .

では、なぜMultimethodsを一緒にグループ化する機能を追加しないのでしょうか?それは、純粋に実用的な理由であり、私が冒頭の文章で「効率的」という言葉を使った理由でもあります。

Clojureはホスト型言語です。つまり、特に 別の 言語プラットフォームの上で実行されるように設計されています。そして、Clojureを実行させたいプラットフォーム(JVM、CLI、ECMAScript、Objective-C)のほとんどで、ディスパッチするための特別な高パフォーマンスのサポートがあることが判明しています。 だけです。 をディスパッチするための特別な高性能サポートがあります。Clojureのマルチメソッドでは 任意のプロパティ すべての引数 .

つまり、プロトコルはディスパッチする対象を のみを に対して 最初 の引数と のみ をその型に対して(あるいは特殊なケースとして nil ).

これはプロトコルの考え方そのものを制限するものではなく、基礎となるプラットフォームの性能最適化にアクセスするための実際的な選択です。特に、プロトコルは JVM/CLI インターフェースへの些細なマッピングを持つので、非常に高速になります。実際、現在JavaやC#で書かれているClojureの部分をClojure自体で書き換えることができるほど高速です。

Clojureは実はバージョン1.0からすでにプロトコルを備えています。 Seq はプロトコルです。しかし、1.2まではClojureでプロトコルを書くことができず、ホスト言語で書かなければなりませんでした。