合計タイプとポリモーフィズム


10

昨年、私は飛躍して関数型プログラミング言語(F#)を学びました。私が発見した興味深い点の1つは、OOソフトウェアの設計方法にどのように影響するかです。私がオブジェクト指向言語で最も欠けていると思うのは、パターンマッチングと合計タイプの2つです。どこを見ても、差別的な組合で簡単にモデル化される状況が見られますが、パラダイムに不自然に感じられる一部のOO DU実装でバールを表示することに抵抗があります。

これにより、通常or、合計タイプが処理する関係を処理する中間タイプを作成するようになります。また、かなりの分岐につながるようです。Misko Heveryのような人を読んだ場合、優れたOOデザインは多態性による分岐を最小限に抑えることができると彼は示唆しています。

OOコードでできる限り避けたいことの1つは、null値を持つ型です。明らかに、or関係は1つのnull値と1つの非null値を持つタイプによってモデル化できますが、これはnullあらゆる場所でのテストを意味します。異種であるが論理的に関連付けられた型を多態的にモデル化する方法はありますか?設計戦略やパターンは非常に役立つでしょう。あるいは、一般的にオブジェクト指向のパラダイムで、異種の関連タイプについて考える方法です。


3
「適切なOO設計は、ポリモーフィズムによる分岐を最小限に抑えることができます」というものです。つまり、実際のビジネスロジックから初期化/構成コードに分岐を移動します。通常、利点は、「初期化と構成」が、ビジネスロジックで明示的な分岐が必要になるよりも(「実行」の観点からではなく、コード内での)発生がはるかに少ないことです。欠点はそこの余地がないということですか、ターゲット...ビジネスロジック内タイプのオブジェクトに応じて
ティモシーTruckle

3
これはあなたにとって興味深いかもしれません(基本的に、作者は合計タイプを階層としてモデル化し、パターンマッチングをモデル化する方法としてサブクラスでオーバーライドされたメソッドの束を使用します)。また、オブジェクト指向では、Nullオブジェクトパターン(特定のポリモーフィック操作に対して何もしないオブジェクトのみ)を使用してnullチェックを回避できます。
FilipMilovanović18年

複合パターンは、読み取り価値があるかもしれません。
candied_orange 2018年

1
改善したいことの例を挙げていただけますか?
JimmyJames

@TimothyTruckle良い説明ですが、常に「初期化/構成」とは限りません。分岐はメソッドを呼び出すと不可視に発生しますが、動的言語ではクラスを動的に追加できる場合があります。その場合、分岐も動的に変化します。
Frank Hileman、2018年

回答:


15

あなたのように、私は差別された組合がもっと広まったことを望みます。ただし、ほとんどの関数型言語でこれらが有用である理由は、完全なパターンマッチングを提供するためです。これがないと、これらは単純な構文にすぎません。単なるパターンマッチングではなく、完全なパターンマッチングなので、コードがコンパイルされません。すべての可能性をカバーします。これがあなたに力を与えるものです。

合計タイプで役立つことを行う唯一の方法は、それを分解し、そのタイプに応じて分岐することです(たとえば、パターンマッチングによる)。インターフェイスの優れた点は、何かのタイプを気にしないことです。それは、それを次のように扱うことができることがわかっているためですiface。各タイプに固有のロジックは必要ありません。分岐はありません。

これは「関数型コードの方が分岐が多く、OOコードの方が少ない」ではなく、「「関数型言語」は、分岐が義務付けられているユニオンが存在するドメインに適しています-「OO言語」はコードに適していますここで、共通の動作を共通のインターフェースとして公開できます-分岐が少ないように感じるかもしれません。」分岐は、デザインとドメインの機能です。簡単に言うと、「異種だが論理的に関連付けられた型」が共通のインターフェースを公開できない場合は、それらに対してブランチ/パターンマッチングを行う必要があります。これはドメイン/設計の問題です。

Miskoが参照している可能性があるのは、タイプを共通のインターフェースとして公開できる場合、オブジェクト指向機能(インターフェース/ポリモーフィズム)を使用すると、タイプ固有の動作消費ではなくタイプに置くことにより、生活を改善するという一般的な考えですコード。

インターフェースと共用体は互いに逆の一種であることを認識することが重要です。インターフェースはが実装する必要があるものを定義し、共用体はコンシューマーが考慮しなければならないものを定義します。メソッドをインターフェースに追加すると、そのコントラクトが変更されたため、以前にそれを実装していたすべてのタイプを更新する必要があります。ユニオンに新しい型を追加すると、そのコントラクトが変更されたため、ユニオンに対するすべての完全なパターンマッチングを更新する必要があります。それらはさまざまな役割を果たし、システムを「どちらかの方法」で実装できることもありますが、どちらを採用するかは設計上の決定です。どちらも本質的には優れていません。

インターフェース/ポリモーフィズムを使用する利点の1つは、使用するコードの拡張性が高いことです。合意されたインターフェースを公開している限り、設計時に定義されていない型を渡すことができます。反対に、静的ユニオンを使用すると、ユニオンの規約に忠実である限り、新しい完全なパターンマッチングを作成することで、設計時に考慮されなかった動作を利用できます。


「Null Object Pattern」について:これは特効薬ではなくnullチェックの代わりにはなりませ。それが行うすべてのことは、「ヌル」動作が共通のインターフェースの背後に公開される可能性があるいくつかの「ヌル」チェックを回避する方法を提供します。タイプのインターフェースの背後にある「null」動作を公開できない場合は、「これを徹底的にパターンマッチングできればいいのに」と考え、最終的に「分岐」チェックを実行することになります。


4
最後から2番目の段落に関連:en.wikipedia.org/wiki/Expression_problem
jk。

「インターフェースは型が実装しなければならないものを定義し、共用体はコンシューマーが考慮しなければならないものを定義します」-インターフェースをそのように見る必要はありません。コンポーネントは必要なインターフェースを定義できます -他のコンポーネントが実装しなければならないもの; そして、提供インタフェース -消費者の成分(すなわち、に対してプログラムすること)を検討していること1。
FilipMilovanović18年

@FilipMilovanovićああ、私はそこまで正確ではありませんでした。私は、ユニオン(コンシューマー->ユニオン->タイプ)の「線形」依存関係ではなく、インターフェース(コンシューマー->インターフェース<-インプリメンター/タイプ)の依存関係の「三角形」に入らないようにしようとしていました。 「意思決定」が行われている場所(たとえば、このタイプが提示された場合に何を行うかをどこで定義するか)を表現しようとするだけ
VisualMelon

3

sum型をオブジェクト指向言語にエンコードするかなり「標準的な」方法があります。

次に2つの例を示します。

type Either<'a, 'b> = Left of 'a | Right of 'b

C#では、これを次のようにレンダリングできます。

interface Either<A, B> {
    C Match<C>(Func<A, C> left, Func<B, C> right);
}

class Left<A, B> : Either<A, B> {
    private readonly A a;
    public Left(A a) { this.a = a; }
    public C Match<C>(Func<A, C> left, Func<B, C> right) {
        return left(a);
    }
}

class Right<A, B> : Either<A, B> {
    private readonly B b;
    public Right(B b) { this.b = b; }
    public C Match<C>(Func<A, C> left, Func<B, C> right) {
        return right(b);
    }
}

再びF#:

type List<'a> = Nil | Cons of 'a * List<'a>

再びC#:

interface List<A> {
    B Match<B>(B nil, Func<A, List<A>, B> cons);
}

class Nil<A> : List<A> {
    public Nil() {}
    public B Match<B>(B nil, Func<A, List<A>, B> cons) {
        return nil;
    }
}

class Cons<A> : List<A> {
    private readonly A head;
    private readonly List<A> tail;
    public Cons(A head, List<A> tail) {
        this.head = head;
        this.tail = tail;
    }
    public B Match<B>(B nil, Func<A, List<A>, B> cons) {
        return cons(head, tail);
    }
}

エンコーディングは完全に機械的です。このエンコーディングは、代数的データ型同じ利点と欠点のほとんどを備えた結果を生成します。また、これをビジターパターンのバリエーションとして認識することもできます。パラメータを収集してMatch、Visitorと呼ぶことができるインターフェースにまとめることができます。

利点の面では、これにより、合計タイプの原則的なエンコーディングが得られます。(これはスコットエンコーディングです。)一度に1つのマッチングの「レイヤー」だけですが、徹底的な「パターンマッチング」を提供します。Matchある意味では、これらのタイプの「完全な」インターフェースであり、必要に応じて追加の操作を定義できます。これは、Ryathalの回答で示したNull ObjectパターンやStateパターン、VisitorパターンやCompositeパターンなど、多くのOOパターンについて異なる見方を示しています。Option/ Maybeタイプは、一般的なヌルオブジェクトのパターンのようなものです。複合パターンは、エンコーディングに似ていますtype Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>。状態パターンは基本的に列挙のエンコーディングです。

不利な面では、私が書いたように、このMatchメソッドは、特にLiskov Substitutabilityプロパティを維持したい場合に、どのサブクラスを有意義に追加できるかについていくつかの制約を課します。たとえば、このエンコードを列挙型に適用しても、列挙を有意義に拡張することはできません。列挙を拡張したい場合は、enumand を使用しているかのように、すべての呼び出し元と実装元をどこでも変更する必要がありswitchます。とはいえ、このエンコードは元のエンコードよりもやや柔軟です。たとえば、2つのリストを保持するだけのAppendインプリListメンターを追加して、一定時間の追加を提供できます。これは一緒に追加されたリストのように動作しますが、別の方法で表されます。

もちろん、これらの問題の多くはMatch、サブクラスにいくらか(概念的には意図的に)関連付けられているという事実に関係しています。それほど具体的でないメソッドを使用すると、より伝統的なOO設計が得られ、拡張性が取り戻されますが、インターフェイスの「完全性」が失われるため、このタイプの操作を定義できなくなります。インターフェース。他の場所で述べたように、これは表現の問題の現れです。

間違いなく、上記のような設計を体系的に使用して、OOの理想を実現するための分岐の必要性を完全になくすことができます。たとえば、Smalltalkはこのパターンを使用して、ブール演算自体を含めます。しかし、前述の説明が示唆しているように、この「分岐の除去」はかなり幻想です。ブランチを別の方法で実装したところ、同じプロパティの多くがまだ残っています。


1

nullの処理は、nullオブジェクトパターンで実行できます。アイデアは、すべてのメンバーのデフォルト値を返すオブジェクトのインスタンスを作成し、何もしないがエラーにならないメソッドを持つことです。これはnullチェックを完全になくすわけではありませんが、オブジェクトの作成時にnullをチェックし、nullオブジェクトを返すだけでよいことを意味します。

状態パターンは、分岐を最小限に抑え、パターンマッチングの利点のいくつかを与えるための方法です。ここでも、分岐ロジックをオブジェクト作成にプッシュします。各状態はベースインターフェースの個別の実装であるため、すべての使用コードはDoStuff()を呼び出すだけで適切なメソッドが呼び出されます。一部の言語では、パターンマッチングを機能として追加しています。C#はその一例です。


(Un?)皮肉にも、これらは両方とも、区別された共用体タイプをOOPにエンコードする「標準」の方法の例です。
Derek Elkinsが
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.