私は最近、主にF#を介して関数型プログラミングに慣れ始めたばかりで、その利点を完全に理解していない特定の関数型イディオムがあります。いくつかの場所で説明されているのを見たことがありますが、バージョン この記事では を参照します。
この記事では、Empty
、Active
(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
Empty
とActive
の状態のみに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
クラスで置き換え、そのコンストラクターを内部にすることです。これは理想的ではなく、状況によっては実行できない場合もありますが、一般的にはかなりうまくいくと思います。
あるisICart.AddItem
ではなくaddItemToCart
を使用することによるメリットはありません。それらは本質的に同じです。いずれの種類のCart
を含む変数を含めることができ、コンパイル時にどちらが関数に渡されるかわからないため、これらの両方にランタイムチェックが必要です。
利点は、最初にaddItemToCart
を実装するとき、または後で同じレベルの抽象化で別の操作を追加するとき、たとえばoneClickAddAndPay
です。ユニオンタイプは、Add
カートにアイテムをPaidFor
しようとする場合、またはPaidFor
カートを完全に考慮することを完全に忘れた場合、コンパイルエラーになります。 ICart
インターフェイスは、コンパイル時にそのようなプログラミングエラーをキャッチできません。
言い換えると、共用体タイプはすべての種類のエラーをコンパイル時に移動することはできませんが、someは移動します。既存のoneClickAddAndPay
機能を再利用するAdd
のような多くの関数を追加しないと、あまり購入できません。
あなたの例では私は利点がないと思いますが、他の言語( Ceylon など)は有用な方法で共用体型を使用しています。このようなことを想像してみてください(C#)
int Length<A> (String | List<A> obj) {...}
StringはIEnumerableを実装しているため、この例はあまり意味がありませんが、この背後にある考え方は、さまざまなタイプのパラメータを受け取り、それらのタイプに自然に共通するフィールド/プロパティ/メソッドを使用し、スイッチ/ケースを使用することです。タイプに依存する動作が必要な場合。
F#では、このような小さなことを実行するためにいくつかのヘルパー型を作成できると思います。私はセイロンの方法を好む:ポリモーフィズム+(主に匿名)ユニオンタイプ。
ユニオンタイプは、null
がタイプNull
の唯一のインスタンスであると想定した場合、正確さを高めるのに役立ちます。次に、何かがnull
であることを宣言したい場合は、タイプ T | Null
またはその略語T?
。