web-dev-qa-db-ja.com

合計タイプと多態性

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

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

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

10
Patrick D

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

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

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

Miskoが参照している可能性があるのは、can型を共通インターフェイスとして公開できる場合、OO機能(インターフェイス/ポリモーフィズム)を使用すると、タイプ固有の動作を使用するコードではなくタイプに置くことで、あなたの人生をより良くします。

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

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


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

15
VisualMelon

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と呼ぶことができるインターフェイスにまとめることができます。

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

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

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

間違いなく、上記のような設計を体系的に使用して、OO理想を実現するための分岐の必要性を完全に排除できます。たとえば、Smalltalkは、ブール自体を含めてこのパターンをよく使用します。ただし、前述のように議論の結果、この「分岐の削除」はかなり幻想的であり、分岐を別の方法で実装しただけで、依然として同じ特性の多くを持っています。

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

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

1
Ryathal