web-dev-qa-db-ja.com

型に対するパターンマッチングは慣用的または貧弱な設計ですか?

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()の実装について心配する必要はありません。

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

18
Larry OBrien

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"

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

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

一般に、FPは、最初に型を設計し、次に関数を追加することに重点を置いています。そのため、タイプ(ドメインモデル)を1つのファイルの数行に収め、ドメインモデルで動作する関数を簡単に追加できるというメリットがあります。

2つのアプローチ-OOとFPは非常に相補的であり、どちらにも長所と短所があります。 (OOの観点から)トリッキーなことは、F#が通常FPスタイルをデフォルトとして使用することです。しかし、本当に新しいサブクラスを追加する必要がある場合は、いつでもインターフェースを使用できます。しかし、ほとんどのシステムでは、型と関数を同様に追加する必要があるため、選択はそれほど重要ではなく、F#で識別された共用体を使用する方が優れています。

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

21
Tomas Petricek

パターンマッチング(本質的にはスーパーチャージャー付きの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)

neverBoolまたはOptionの別のサブタイプになるため、サブクラス化は役に立たないようです(Scalaこれを処理できるサブクラス化の概念があります。クラスは現在のコンパイル単位の外部で「最終」としてマークできますが、サブタイプはこのコンパイル単位の内部で定義できます)。

Optionのような型のサブタイプが静的に既知になったため、パターンマッチで大文字と小文字を処理し忘れた場合、コンパイラーは警告を出すことができます。つまり、パターンマッチは、すべてのオプションを処理するように強制する特別なダウンキャストのようなものです。

さらに、動的メソッドディスパッチ(OOPに必要)もランタイムタイプチェックを意味しますが、種類は異なります。したがって、この型チェックをパターンマッチを通じて明示的に行うか、メソッド呼び出しを通じて暗黙的に行うかは、あまり関係ありません。

8
amon

F#パターンマッチングは通常、クラスではなく識別された共用体で行われます(したがって、技術的には型チェックではありません)。これにより、パターンマッチのケースが考慮されていない場合にコンパイラーが警告を出すことができます。

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

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

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

部分的には、型を使用してより頻繁に決定を行うため、関数型プログラミングでより頻繁に見られます。多分ランダムに例を選んだだけだと思いますが、パターンマッチングの例と同等のOOPは次のようになります。

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

つまり、OOPでのnullチェックなどの日常的なことを回避するためにポリモーフィズムを使用することは比較的まれです。 OOプログラマーが nullオブジェクト を作成しないのと同じように、関数型プログラマーは常に関数をオーバーロードするわけではありませんより多くの状況で型システムを使用すると、慣れていない方法で使用されていることがわかります。

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

では、パターンマッチングはいつ、優れた関数型プログラミングコードと見なされるのでしょうか。タイプを調べるだけではなく、要件を拡張するときにケースを追加する必要はありません。たとえば、Some valは、optの型がSomeであることを確認するだけでなく、val->の反対側でタイプセーフに使用できるように、基になる型にバインドします。 3番目のケースが必要になることはほとんどないため、これは有効な用途です。

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

2
Karl Bielefeldt