型に対するパターンマッチングは慣用的ですか、またはデザインが貧弱ですか?


18

F#コードは多くの場合、型に対するパターンマッチのようです。もちろん

match opt with 
| Some val -> Something(val) 
| None -> Different()

よくあるようです。

しかし、OOPの観点から見ると、それはランタイムタイプチェックに基づく制御フローに非常によく似ており、通常は眉をひそめます。わかりやすく言うと、OOPではおそらくオーバーロードを使用することをお勧めします。

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

これは確かにより多くのコードです。OTOH、私のOOP-yの心には構造的な利点があるようです:

  • 新しい形式への拡張Tは簡単です。
  • ルートを選択する制御フローの重複を見つけることを心配する必要はありません。そして
  • ルートの選択は不変です。一度Foo手に入れれば、Bar.Route()の実装を心配する必要はありません。

表示されていない型に対するパターンマッチングの利点はありますか?それは慣用的と考えられますか、それとも一般的に使用されない機能ですか?


3
OOPの観点から関数型言語を表示することには、どの程度の意味がありますか?とにかく、パターンマッチングの真の力は、ネストされたパターンにあります。一番外側のコンストラクターをチェックするだけでも可能ですが、決してストーリー全体ではありません。
インゴ14

これBut from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.は、あまりにも独断的に聞こえます。場合によっては、オペレーションを階層から分離する必要があります。多分1)階層にopを追加できないb / c階層を所有していない。2)opにしたいクラスが階層と一致しません。3)opを階層に追加することはできますが、ほとんどのクライアントが使用しないがらくたの塊で階層のAPIを乱雑にしたくない場合は、b / cをしたくありません。

4
明確にするためだけで、タイプSomeNoneはありません。型はforall a. a -> option aandであるコンストラクターですforall a. option a(申し訳ありませんが、F#の型注釈の構文は不明です)。

回答:


20

OOPクラス階層はF#の識別されたユニオンと非常に密接に関連しており、パターンマッチングは動的型テストと非常に密接に関連しているという点で正しいです。実際、これは実際にF#が差別されたユニオンを.NETにコンパイルする方法です!

拡張性に関して、問題には2つの側面があります。

  • OOでは、新しいサブクラスを追加できますが、新しい(仮想)関数を追加するのは難しくなります
  • FPを使用すると、新しい関数を追加できますが、新しいユニオンケースを追加することは困難です

そうは言っても、パターンマッチングでケースを見逃した場合、F#から警告が表示されるため、新しいユニオンケースを追加しても実際にはそれほど悪くはありません。

ルート選択で重複を見つけることについて-F#は、一致する一致がある場合に警告を表示します。例:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

「ルートの選択は不変」という事実にも問題があるかもしれません。たとえば、Fooとの間で関数の実装を共有Barしたいが、そのZooケースに対して別のことをしたい場合、パターンマッチングを使用して簡単にエンコードできます。

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

一般に、FPは、最初に型を設計し、次に関数を追加することに重点を置いています。そのため、単一のファイルの数行に型(ドメインモデル)を適合させ、ドメインモデルで動作する関数を簡単に追加できるという事実から本当に恩恵を受けます。

2つのアプローチ-OOとFPは非常に補完的であり、どちらにも利点と欠点があります。難しいこと(オブジェクト指向の観点から)は、F#が通常FPスタイルをデフォルトとして使用することです。ただし、新しいサブクラスを追加する必要性が本当にある場合は、いつでもインターフェイスを使用できます。しかし、ほとんどのシステムでは、同様に型と関数を追加する必要があるため、選択はそれほど重要ではありません。また、F#で識別されたユニオンを使用する方が適切です。

詳細については、この素晴らしいブログシリーズをお勧めします。


3
あなたは正しいですが、オブジェクト対合計タイプの問題であるため、これはオブジェクト指向とFPの問題ではないことを付け加えたいと思います。オブジェクトに対するOOPの執着はさておき、オブジェクトが機能しなくなるオブジェクトについては何もありません。そして、あなたが十分なフープを飛び越えれば、主流のOOP言語でも合計型を実装できます(それはきれいではありませんが)。
ドーバル14

1
「そして、あなたが十分なフープを飛び越えれば、主流のOOP言語でもsumタイプを実装できます(それはきれいではありませんが)。」-> .NETの型システムでF#和型がどのようにエンコードされるかに似たものになると思います:)
Tarmil

7

パターンマッチング(基本的にはスーパーチャージされたswitchステートメント)と動的ディスパッチには類似性があることを正しく確認しました。また、いくつかの言語で共存し、非常に楽しい結果をもたらします。ただし、わずかな違いがあります。

型システムを使用して、固定数のサブタイプのみを持つことができる型を定義できます。

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

そこだろう決して他のサブタイプになることはありませBoolもしくはOption有用であることが表示されませんサブクラス化して(Scalaのようないくつかの言語はそれがこれを扱うことができるサブクラス化の概念を持っている、 -クラスが「最終」現在のコンパイル単位の外ではなく、サブタイプとしてマークすることができますこのコンパイル単位内で定義できます)。

のような型のサブタイプは静的に認識されるようOptionになったため、パターンマッチでケースを処理するのを忘れると、コンパイラは警告を出すことができます。つまり、パターンマッチは、すべてのオプションを処理することを余儀なくされる特別なダウンキャストのようなものです。

さらに、動的メソッドディスパッチ(OOPに必要)は、実行時の型チェックも意味しますが、種類は異なります。したがって、パターンマッチを介して明示的にこのタイプチェックを行う場合、またはメソッド呼び出しを介して暗黙的にこのタイプチェックを行う場合、それはかなり無関係です。


「これは、パターンマッチが、すべてのオプションを処理することを余儀なくされる特別なダウンキャストのようなものであることを意味します」-実際、(値またはネストされた構造ではなく、コンストラクターとのみマッチングしている限り)それは同型であると信じていますスーパークラスに抽象仮想メソッドを配置します。
ジュール

2

F#パターンマッチングは、通常、クラスではなく判別されたユニオンを使用して実行されます(したがって、技術的には型チェックではありません)。これにより、パターンマッチのケースに対応していない場合、コンパイラは警告を表示できます。

もう1つ注意すべきことは、機能的なスタイルでは、データではなく機能ごとに整理するため、パターンマッチを使用すると、クラス全体に散らばるのではなく、さまざまな機能を1か所に集めることができます。これには、変更を加える必要がある場所のすぐ隣で他のケースがどのように処理されるかを確認できるという利点もあります。

新しいオプションを追加すると、次のようになります。

  1. 差別化された組合に新しいオプションを追加する
  2. 不完全なパターンマッチに関するすべての警告を修正

2

部分的に、型を使用してより頻繁に決定を下すため、関数型プログラミングでより頻繁に表示されます。多かれ少なかれランダムに例を選んだと思いますが、パターンマッチングの例に相当するOOPは次のよ​​うになります。

if (opt != null)
    opt.Something()
else
    Different()

言い換えれば、OOPでのnullチェックなどの日常的なことを避けるためにポリモーフィズムを使用することは比較的まれです。OOプログラマーがあらゆる小さな状況でnullオブジェクトを作成しないように、機能的プログラマーは、特にパターンのリストが網羅的であることが保証されていることがわかっている場合、常に関数をオーバーロードするわけではありません。より多くの状況で型システムを使用する場合、慣れていない方法で使用されていることがわかります。

逆に、OOPの例に相当する慣用的な関数型プログラミングでは、パターンマッチングを使用しない可能性が高くなりますが、呼び出しコードに引数として渡される関数がfooRouteありbarRouteます。そのような状況で誰かがパターンマッチングを使用した場合、OOPで型を切り替える誰かが間違っていると見なされるように、通常は間違っていると見なされます。

それでは、パターンマッチングはいつ優れた関数型プログラミングコードと見なされますか?タイプを見ているだけでなく、要件を拡張するときにケースを追加する必要はありません。たとえば、type Some valopt持つを単にチェックするのではなく、の反対側でタイプセーフな使用のために基礎となるタイプにSomeバインドval->ます。3番目のケースが必要になることはほとんどないことを知っているので、それは良い使い方です。

パターンマッチングは、表面的にはオブジェクト指向のswitchステートメントに似ていますが、特に長いパターンやネストされたパターンでは、さらに多くのことが行われます。設計が不十分なOOPコードと同等であると宣言する前に、実行していることをすべて考慮してください。多くの場合、継承階層できれいに表現できない状況を簡潔に処理しています。


私はあなたがこれを知っている知っている、とあなたの答えを書きながら、それはおそらくあなたの心を滑って、しかしなお、Some及びNoneあなたはタイプのパターンマッチングではありませんので、種類ではありません。同じタイプのコンストラクターでパターンマッチします。これは、「instanceof」を尋ねるようなものではありません。
アンドレスF.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.