web-dev-qa-db-ja.com

インターフェイス(OOP)のセマンティックコントラクトは、関数シグネチャ(FP)よりも有益ですか?

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
}
_
16
AlexFoxGill

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)に置き換える代わりに、パラメーター名を型名に置き換えることができます。たとえば、intIdと呼ばれる型にラップできます。

    type Id = Id of int
    

    これで、read署名は次のようになります。

    let read (id:int) (messageReader : Id -> string) = // ...
    

    これは、以前と同じように有益です。

    ちなみに、これは、OOPにあったコンパイラー保護の一部も提供します。 OOPバージョンでは、古いint -> string関数だけでなくIMessageQueryを具体的に採用していることを確認しましたが、ここでは同様の(ただし異なる)保護があります。古いId -> stringだけではなくint -> stringを取得します。


これらの手法は常に、インターフェイスで完全な情報を利用できるのと同じくらい優れており、有益であると100%自信を持って言いたくありませんが、上記の例から、ほとんどの場合、おそらく同じくらい良い仕事をすることができます。

8
Ben Aaronson

FPを実行するとき、私はより具体的なセマンティックタイプを使用する傾向があります。

たとえば、私にとってあなたの方法は次のようになります:

_read: MessageId -> Message
_

これは、OO(/ Java)スタイルのThingDoer.doThing()スタイルよりもはるかに多くの情報をやり取りします

10
Daenyth

では、関数型プログラマは、名前の付いたインターフェイスのセマンティクスを維持するために何ができるでしょうか。

適切な名前の関数を使用します。

IMessageQuery::Read: int -> stringは単にReadMessageQuery: int -> stringまたは類似したものになります。

注意すべき重要な点は、名前は単語の最も緩い意味での契約にすぎないということです。これらは、あなたと他のプログラマーが名前から同じ意味を推測して従う場合にのみ機能します。このため、暗黙の動作を伝えるany名を実際に使用できます。 OOと関数型プログラミングでは、名前がわずかに異なる場所にあり、形状がわずかに異なりますが、機能は同じです。

インターフェイス(OOP)のセマンティックコントラクトは、関数シグネチャ(FP)よりも有益ですか?

この例ではありません。上で説明したように、単一の関数を含む単一のクラス/インターフェースは、同様に名前が付けられたスタンドアロン関数よりも意味がありません。

クラスで複数の関数/フィールド/プロパティを取得すると、それらの関係を確認できるため、それらについてより多くの情報を推測できます。名前空間またはモジュールによって編成された同じまたは類似のパラメーターを使用するスタンドアロン関数またはスタンドアロン関数よりも情報が多いかどうかは、議論の余地があります。

個人的には、OOは、より複雑な例であっても、かなり有益であるとは思わない。

9
Telastyn

単一の関数が「セマンティックコントラクト」を持つことはできないことに同意します。 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;

型チェッカーは型に割り当てたセマンティクスを適用しないので、すべてのコントラクトに新しい型を作成するのは単なる定型文です。

0
Jonathan Cast