web-dev-qa-db-ja.com

共用体型は、一般的なインターフェースよりも正確さに関してどのように優れていますか?

私は最近、主にF#を介して関数型プログラミングに慣れ始めたばかりで、その利点を完全に理解していない特定の関数型イディオムがあります。いくつかの場所で説明されているのを見たことがありますが、バージョン この記事では を参照します。

この記事では、EmptyActive(1つ以上のアイテムが入っているがまだ支払われていない)、またはPaidForの3つの状態のいずれかに本質的に入るいくつかの要件を持つショッピングカートについて説明します。 F#を使用することにより、コンパイラーがカートの状態に対して不正な操作を実行するのを防ぐため、正確性を簡単に確保できると主張されています。例えば:

let addItemToCart cart item =  
   match cart with
   | Empty state -> state.Add item
   | Active state -> state.Add item
   | PaidFor state ->  
       printfn "ERROR: The cart is paid for"
       cart   

EmptyActiveの状態のみにAddメソッドがアタッチされているため、代わりに次のように記述しようとすると、

| PaidFor state -> state.Add item

または同様の場合、コンパイラエラーが発生します。

比較すると、OOアプローチは(C#では)次のようになります。

interface ICart
{
    ICart AddItem(CartItem item);
}

class EmptyCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        return new ActiveCart(item);
    }
}

class ActiveCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        return new ActiveCart(_items.Add(item));
    }
}

class PaidForCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        throw new InvalidOperationException("Who cares about Liskov anyway?");
    }
}

私が理解しようとしているのは、クライアントがICart.AddItemではなくaddItemToCartを使用することで得られるメリットです。どちらの方法でも、カートが有料の場合にクライアントがその関数を使用できないようにするコンパイル時のチェックはありません。いずれにしても、クライアントはエラーの場合に何が起こるかを制御できません。

機能バージョンは、エラーの場合に何が起こるか(コールバック、または結果に対するラッパータイプ)をクライアントに制御するように変更できますが、オブジェクト指向も同じように簡単に変更できます(たぶんTryAddItemメソッド。これは、もう少しオブジェクト指向的なものになります)。

それで私は何が欠けていますか?

編集

C#バージョンでは、誰でもICart実装を作成できるというコメントで言及されました。これに対処するには:代替バージョンは、ICartを抽象的なCartクラスで置き換え、そのコンストラクターを内部にすることです。これは理想的ではなく、状況によっては実行できない場合もありますが、一般的にはかなりうまくいくと思います。

5
Ben Aaronson

あるisICart.AddItemではなくaddItemToCartを使用することによるメリットはありません。それらは本質的に同じです。いずれの種類のCartを含む変数を含めることができ、コンパイル時にどちらが関数に渡されるかわからないため、これらの両方にランタイムチェックが必要です。

利点は、最初にaddItemToCartを実装するとき、または後で同じレベルの抽象化で別の操作を追加するとき、たとえばoneClickAddAndPayです。ユニオンタイプは、AddカートにアイテムをPaidForしようとする場合、またはPaidForカートを完全に考慮することを完全に忘れた場合、コンパイルエラーになります。 ICartインターフェイスは、コンパイル時にそのようなプログラミングエラーをキャッチできません。

言い換えると、共用体タイプはすべての種類のエラーをコンパイル時に移動することはできませんが、someは移動します。既存のoneClickAddAndPay機能を再利用するAddのような多くの関数を追加しないと、あまり購入できません。

6
Karl Bielefeldt

あなたの例では私は利点がないと思いますが、他の言語( Ceylon など)は有用な方法で共用体型を使用しています。このようなことを想像してみてください(C#)

int Length<A> (String | List<A> obj) {...}

StringはIEnumerableを実装しているため、この例はあまり意味がありませんが、この背後にある考え方は、さまざまなタイプのパラメータを受け取り、それらのタイプに自然に共通するフィールド/プロパティ/メソッドを使用し、スイッチ/ケースを使用することです。タイプに依存する動作が必要な場合。

F#では、このような小さなことを実行するためにいくつかのヘルパー型を作成できると思います。私はセイロンの方法を好む:ポリモーフィズム+(主に匿名)ユニオンタイプ。

ユニオンタイプは、nullがタイプNullの唯一のインスタンスであると想定した場合、正確さを高めるのに役立ちます。次に、何かがnullであることを宣言したい場合は、タイプ T | Nullまたはその略語T?

2
Cristian Garcia