SOLID原理を極端にすると、最終的には関数型プログラミングになります と言われています。私はこの記事に同意しますが、いくつかの意味論はインターフェイス/オブジェクトから関数/クロージャへの移行で失われました。関数型プログラミングがその損失をどのように軽減できるか知りたいです。
記事から:
さらに、インターフェイス分離原則(ISP)を厳密に適用すると、ヘッダーインターフェイスよりもロールインターフェイスを優先する必要があることがわかります。
デザインをどんどん小さくしていくインターフェースを設計し続けると、最終的には究極のロールインターフェース、つまり単一のメソッドを持つインターフェースにたどり着きます。これは私によく起こります。次に例を示します。
_public interface IMessageQuery
{
string Read(int id);
}
_
IMessageQuery
に依存する場合、暗黙の契約の一部は、Read(id)
を呼び出すと、指定されたIDのメッセージを返します。
これを、同等の機能シグニチャー_int -> string
_への依存関係と比較してください。追加の手がかりがなければ、この関数は単純なToString()
になります。 IMessageQuery.Read(int id)
をToString()
で実装した場合、意図的に破壊的であると非難するかもしれません!
では、関数型プログラマは、名前の付いたインターフェイスのセマンティクスを維持するために何ができるでしょうか。たとえば、単一のメンバーでレコードタイプを作成することは一般的ですか?
_type MessageQuery = {
Read: int -> string
}
_
Telastynが言うように、関数の静的な定義を比較すると:
public string Read(int id) { /*...*/ }
に
let read (id:int) = //...
OOPからFPまで、何も失われていません。
ただし、関数とインターフェースは静的な定義で参照されるだけではないため、これは話の一部にすぎません。また、渡されます。したがって、MessageQuery
が別のコードMessageProcessor
によって読み取られたとしましょう。それから私達は持っています:
public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }
今では直接メソッド名IMessageQuery.Read
またはそのパラメータint id
を見ることができませんが、私たちのIDE。より一般的には、intからstringへの関数のメソッドを持つ単なるインターフェイスではなくIMessageQuery
を渡すという事実は、この関数に関連付けられたid
パラメータ名メタデータを保持していることを意味します。
一方、機能バージョンでは次のようになります。
let read (id:int) (messageReader : int -> string) = // ...
それで、私たちは何を保持して失ったのですか?ええと、パラメーター名messageReader
がまだあるので、おそらく型名(IMessageQuery
と同等)が不要になります。しかし、今では関数のパラメーター名id
が失われています。
これを回避するには、主に2つの方法があります。
まず、その署名を読むことで、何が起こっているのかをかなりよく推測できます。関数を短く、シンプルでまとまりのあるものにし、適切な名前を付けることで、この情報を直感的に見つけたり、見つけたりすることが非常に簡単になります。実際の関数自体を読み取ると、さらに簡単になります。
次に、- 多くの関数型言語では慣用的な設計と見なされます プリミティブをラップする小さな型を作成します。この場合、逆のことが起こります。型名をパラメーター名(IMessageQuery
からmessageReader
)に置き換える代わりに、パラメーター名を型名に置き換えることができます。たとえば、int
はId
と呼ばれる型にラップできます。
type Id = Id of int
これで、read
署名は次のようになります。
let read (id:int) (messageReader : Id -> string) = // ...
これは、以前と同じように有益です。
ちなみに、これは、OOPにあったコンパイラー保護の一部も提供します。 OOPバージョンでは、古いint -> string
関数だけでなくIMessageQuery
を具体的に採用していることを確認しましたが、ここでは同様の(ただし異なる)保護があります。古いId -> string
だけではなくint -> string
を取得します。
これらの手法は常に、インターフェイスで完全な情報を利用できるのと同じくらい優れており、有益であると100%自信を持って言いたくありませんが、上記の例から、ほとんどの場合、おそらく同じくらい良い仕事をすることができます。
FPを実行するとき、私はより具体的なセマンティックタイプを使用する傾向があります。
たとえば、私にとってあなたの方法は次のようになります:
_read: MessageId -> Message
_
これは、OO(/ Java)スタイルのThingDoer.doThing()
スタイルよりもはるかに多くの情報をやり取りします
では、関数型プログラマは、名前の付いたインターフェイスのセマンティクスを維持するために何ができるでしょうか。
適切な名前の関数を使用します。
IMessageQuery::Read: int -> string
は単にReadMessageQuery: int -> string
または類似したものになります。
注意すべき重要な点は、名前は単語の最も緩い意味での契約にすぎないということです。これらは、あなたと他のプログラマーが名前から同じ意味を推測して従う場合にのみ機能します。このため、暗黙の動作を伝えるany名を実際に使用できます。 OOと関数型プログラミングでは、名前がわずかに異なる場所にあり、形状がわずかに異なりますが、機能は同じです。
インターフェイス(OOP)のセマンティックコントラクトは、関数シグネチャ(FP)よりも有益ですか?
この例ではありません。上で説明したように、単一の関数を含む単一のクラス/インターフェースは、同様に名前が付けられたスタンドアロン関数よりも意味がありません。
クラスで複数の関数/フィールド/プロパティを取得すると、それらの関係を確認できるため、それらについてより多くの情報を推測できます。名前空間またはモジュールによって編成された同じまたは類似のパラメーターを使用するスタンドアロン関数またはスタンドアロン関数よりも情報が多いかどうかは、議論の余地があります。
個人的には、OOは、より複雑な例であっても、かなり有益であるとは思わない。
単一の関数が「セマンティックコントラクト」を持つことはできないことに同意します。 foldr
に関する以下の法律を考慮してください:
foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn
それはどのような意味で意味的ではなく、契約ではないのですか?特にfoldr
はこれらの法則によって一意に決定されるため、「フォルダー」のタイプを定義する必要はありません。あなたはそれが何をするのか正確に知っています。
あるタイプの関数を取り込みたい場合は、同じことを行うことができます。
-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;
同じ契約を複数回必要とする場合にのみ、そのタイプに名前を付けてキャプチャする必要があります。
-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;
型チェッカーは型に割り当てたセマンティクスを適用しないので、すべてのコントラクトに新しい型を作成するのは単なる定型文です。