スイッチ/パターンマッチングのアイデア


151

私は最近F#を検討してきましたが、すぐにフェンスを飛び越えそうにありませんが、C#(またはライブラリのサポート)が生活を楽にする可能性のあるいくつかの領域をはっきりと強調しています。

特に、F#のパターンマッチング機能について考えています。これにより、非常に豊富な構文が可能になり、現在のスイッチ/条件付きC#の同等機能よりもはるかに表現力が豊かになります。私は直接的な例を挙げようとはしませんが(私のF#はそれまでではありません)、簡単に言えば次のことが可能です。

  • 型による一致(識別された共用体の完全なカバレッジチェックを使用)[これにより、バインドされた変数の型も推測され、メンバーにアクセス権が与えられることに注意してください]
  • 述語で一致
  • 上記の組み合わせ(そしておそらく私が知らない他のいくつかのシナリオ)

C#がこの豊富さの一部を最終的に借りるのは素晴らしいことですが、暫定的には、実行時に何ができるかを検討してきました。たとえば、いくつかのオブジェクトを組み合わせると、次のようになります。

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

ここで、getRentPriceはFunc <Vehicle、int>です。

[注-ここのスイッチ/ケースはおそらく間違った用語です...しかしそれは考えを示しています]

私にとって、これは、if / elseを繰り返し使用した同等のものや、複合3項条件式(自明ではない式では非常に厄介です-大括弧)を使用した場合よりもはるかに明確です。また、多くのキャストを回避し、VB Select ... Case "x To yに相当するInRange(...)一致など、より具体的な一致への(直接または拡張メソッドによる)単純な拡張を可能にします。 " 使用法。

(言語サポートがない場合)上記のような構造から多くの利点があると人々が考えるかどうかを測定しようとしているだけですか?

さらに、私は上記の3つのバリアントで遊んでいることに注意してください。

  • 評価用のFunc <TSource、TValue>バージョン-複合3項条件ステートメントに相当
  • Action <TSource>バージョン-if / else if / else if / else if / elseに相当
  • Expression <Func <TSource、TValue >>バージョン-最初のものとして、任意のLINQプロバイダーが使用可能

さらに、式ベースのバージョンを使用すると、式ツリーの書き換えが可能になり、呼び出しを繰り返し使用するのではなく、本質的にすべてのブランチを単一の複合条件式にインライン化します。私は最近チェックしていませんが、初期のEntity Frameworkビルドでは、InvocationExpressionがあまり好きではなかったため、これが必要であることを思い出しているようです。また、デリゲートの呼び出しの繰り返しを回避できるため、LINQ-to-Objectsを使用してより効率的に使用できます。テストでは、同等のC#と比較して同じ速度(実際にはわずかに高速)で実行される上記のような(式フォームを使用した)一致が示されます。複合条件ステートメント。完全を期すために、Func <...>ベースのバージョンはC#の条件付きステートメントの4倍の時間がかかりましたが、それでも非常に高速であり、ほとんどのユースケースで主要なボトルネックになることはほとんどありません。

私は、上記(またはより豊富なC#言語サポートの可能性についての考え、入力、批評など)を歓迎します。


「私は、人々が(言語サポートがない場合)上記のような構造から多くの利益があると思うかどうかを測定しようとしているだけですか?」私見、はい。似たようなものはすでに存在していませんか?そうでない場合は、軽量ライブラリを作成することをお勧めします。
Konrad Rudolph、

10
select caseステートメントでこれをサポートするVB .NETを使用できます。イーク!
ジムバーガー

私も自分自身のホーンをプープーとマイライブラリへのリンクを追加します:機能-DOTNET
アレクセイ・ロマノフ

1
私はこのアイデアが好きで、スイッチケースの非常に素晴らしく、はるかに柔軟な形になります。ただし、これは実際にはLinqのような構文をif-thenラッパーとして使用する装飾された方法ではないでしょうか。私は誰かがこれを本当の取引、つまりswitch-case声明の代わりに使うことを思いとどまらせます。私を誤解しないでください、それはそれがある場所だと思います、そして私はおそらく実装する方法を探すでしょう。
IAbstract

2
この質問は2年以上前のものですが、パターンマッチング機能を備えたC#7がまもなく登場します。
Abion47 16

回答:


22

私はそれが古いトピックであることを知っていますが、c#7ではあなたが行うことができます:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

ここでのC#とF#の顕著な違いは、パターンマッチの完全性です。パターンマッチは、可能であれば、完全に記述された、コンパイラからのすべての可能な警告をカバーします。デフォルトのケースがこれを行うと正当に主張することはできますが、実際には多くの場合、実行時例外です。
VoronoiPotato

37

このような "機能的な"ことをC#で実行しようとすると(そしてそれについての本を試そうとしても)、結局、いくつかの例外を除いて、そのようなことはあまり役に立たないという結論に達しました。

主な理由は、F#などの言語がこれらの機能を真にサポートすることで、多くの能力を発揮するためです。「できる」ではなく、「シンプルで明快、期待通り」。

たとえば、パターンマッチングでは、不完全な一致があるかどうか、または別の一致がヒットしない場合にコンパイラーから通知されます。これはオープンエンド型ではあまり役に立ちませんが、識別された共用体またはタプルを照合する場合、非常に気の利いたものです。F#では、パターンマッチを期待しているので、すぐに理解できます。

「問題」とは、いくつかの機能的な概念を使い始めたら、継続したいのは自然なことです。ただし、C#でタプル、関数、メソッドの部分的な適用とカリー化、パターンマッチング、ネストされた関数、ジェネリックス、モナドサポートなどを利用すると、非常醜く、非常に速くなります。それは楽しいですし、非常に賢い人の中にはC#で非常にクールなことをした人もいますが、実際にそれを使用するのは大変です。

私がC#で頻繁に(プロジェクト全体で)使用した結果:

  • IEnumerableの拡張メソッドによるシーケンス関数。ForEachやProcess( "Apply"?-列挙されたシーケンスアイテムに対してアクションを実行します)のようなものは、C#構文で十分にサポートされているため、適切です。
  • 一般的なステートメントパターンを抽象化します。複雑なtry / catch / finallyブロックまたはその他の関連する(多くの場合、一般的な)コードブロック。LINQ-to-SQLの拡張もここに当てはまります。
  • タプル、ある程度。

**ただし、注意してください:自動一般化と型推論の欠如は、これらの機能の使用さえも本当に妨げています。**

他の誰かが述べたように、これはすべて、特定の目的のために、小さなチームで、はい、おそらく、C#に悩まされている場合に役立つ可能性があります。しかし、私の経験では、彼らは通常、価値があるよりも面倒だと感じました-YMMV。

その他のリンク:


25

間違いなく、C#が型を切り替えるのを簡単にしない理由は、それが主にオブジェクト指向言語であり、オブジェクト指向の用語でこれを行う「正しい」方法は、VehicleでGetRentPriceメソッドを定義することであり、派生クラスでオーバーライドします。

そうは言っても、F#やHaskellなどのこの種の機能を備えたマルチパラダイムや関数型言語で少し時間を費やしてみましたが、以前はそれが役立つ場所がたくさんありました(たとえば、スイッチオンする必要がある型を記述していないため、仮想メソッドを実装できません)。これは、差別化された共用体とともに言語に歓迎されます。

[編集:マークが短絡している可能性があることを示したため、パフォーマンスに関する部分を削除]

別の潜在的な問題は、使いやすさの問題です。最後の呼び出しから、一致が条件を満たさなかった場合にどうなるかは明らかですが、2つ以上の条件に一致した場合の動作は何ですか?例外をスローする必要がありますか?最初または最後の一致を返す必要がありますか?

この種の問題を解決するために私がよく使用する方法は、キーとしてタイプ、値としてラムダを持つディクショナリフィールドを使用することです。これは、オブジェクト初期化構文を使用して構築するのがかなり簡単です。ただし、これは具体的なタイプのみを考慮し、追加の述語を許可しないため、より複雑なケースには適さない場合があります。[注意-C#コンパイラの出力を見ると、switchステートメントが辞書ベースのジャンプテーブルに頻繁に変換されるので、型の切り替えをサポートできなかったのには理由がありません]


1
実際、私が持っているバージョンは、デリゲートバージョンと式バージョンの両方で短絡しています。式バージョンは、複合条件にコンパイルされます。デリゲートバージョンは、単なる述語とfunc / actionsのセットです。一致すると、停止します。
Marc Gravell

おもしろい-ざっくりとした外観から、メソッドチェーンのように見えるので、少なくとも各条件の基本的なチェックを実行する必要があると思いましたが、今では、メソッドが実際にオブジェクトインスタンスをチェーンしてビルドしているため、これを行うことができます。回答を編集してそのステートメントを削除します。
グレッグビーチ、

22

この種のライブラリ(言語拡張機能のように機能する)は広く受け入れられる可能性は低いと思いますが、それらは操作するのが楽しく、特定のドメインで作業している小さなチームにとって非常に便利です。たとえば、このような任意の型テストを実行する大量の「ビジネスルール/ロジック」を作成している場合、それがどのように便利であるかがわかります。

これがC#言語機能である可能性が高いかどうかはわかりません(疑わしいようですが、誰が未来を見ることができますか?)。

参考までに、対応するF#はおおよそ次のとおりです。

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

あなたがの線に沿ってクラス階層を定義したと仮定して

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors

2
F#バージョンをありがとう。F#がこれを処理する方法が好きだと思いますが、現時点では(全体として)F#が正しい選択であるかどうかは
わかりませ

13

あなたの質問に答えるために、はい、パターンマッチング構文構文は有用だと思います。私はそれのためにC#で構文サポートを見たいです。

これは、あなたが説明するのと同じ構文を(ほぼ)提供するクラスの私の実装です

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

ここにいくつかのテストコードがあります:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }

9

パターンマッチング(ここで説明)の目的は、型の仕様に従って値を分解することです。ただし、C#のクラス(または型)の概念はあなたに同意しません。

逆に、マルチパラダイム言語の設計には問題がありません。逆に、C#にラムダを含めると非常に便利です。Haskellは、IOなどに命令的なことを行うことができます。しかし、これは非常にエレガントなソリューションではなく、Haskell形式ではありません。

しかし、シーケンシャルプロシージャプログラミング言語はラムダ計算の観点から理解できるため、C#はシーケンシャルプロシージャ言語のパラメーター内にうまく収まるため、非常に適しています。しかし、Haskellの純粋な関数型のコンテキストから何かを取り出し、その機能を純粋でない言語に組み込むと、まあ、それだけでは、より良い結果が保証されるわけではありません。

私のポイントは、これは、パターンマッチングの目盛りが言語設計とデータモデルに関連付けられていることです。そうは言っても、パターンマッチングはC#の便利な機能であるとは思われません。パターンマッチングは典型的なC#の問題を解決せず、命令型プログラミングパラダイムにうまく適合しないからです。


1
多分。実際、なぜそれが必要になるのかについての説得力のある「キラー」な議論を考えるのに苦労します(「言語をより複雑にすることを犠牲にして、いくつかのエッジケースではおそらく良い」とは対照的です)。
Marc Gravell

5

このようなことを行うOOの方法は、ビジターパターンです。ビジターメンバーのメソッドは、単にケースコンストラクトとして機能し、型自体を「調べる」必要なく、言語自体が適切なディスパッチを処理できるようにします。


4

タイプをオンにするのはあまり「C-sharpey」ではありませんが、コンストラクトは一般的な使用ではかなり役立つことを知っています-私はそれを使用できる少なくとも1つの個人プロジェクトを持っています(ただし、管理可能なATM)。式ツリーを書き直すと、コンパイルのパフォーマンスの問題はたくさんありますか?


オブジェクトを再利用のためにキャッシュする場合(コンパイラーがコードを非表示にすることを除いて、これは主にC#ラムダ式が機能する方法です)。書き直しにより、コンパイル済みのパフォーマンスが確実に向上します。ただし、通常の使用(LINQ-to-Somethingではなく)では、デリゲートバージョンの方が役立つと思います。
Marc Gravell

また、これは必ずしもタイプの切り替えではないことに注意してください。これは、(LINQを介しても)複合条件として使用することもできますが、厄介なx => Testがない場合は?Result1:(Test2?Result2:(Test3?Result 3:Result4))
Marc Gravell

私は実際のコンパイルのパフォーマンスを意味していましたが、知っていて良かったです。csc.exeにかかる時間-それが本当に問題であるかどうかを知るには、C#に詳しくありませんが、C ++の大きな問題です。
Simon Buchan

CSCは、この時に点滅していないだろう-それは、LINQがどのように動作するかに非常に類似しており、C#3.0コンパイラなどのLINQ /拡張メソッドではかなり良いです
マルクGravell

3

これは非常に興味深い(+1)ように見えますが、C#コンパイラはスイッチステートメントの最適化に優れているので、注意が必要です。短絡だけでなく、ケースの数などに応じて完全に異なるILを取得します。

あなたの具体的な例は、私が非常に役立つと思うことを行います-(たとえば)typeof(Motorcycle)定数ではないので、型ごとのケースに相当する構文はありません。

これは動的アプリケーションでさらに興味深いものになります。ここでのロジックは簡単にデータ駆動型であり、「ルールエンジン」スタイルの実行を可能にします。


0

私が書いたOneOfと呼ばれるライブラリを使用することで、あなたは自分の後にあることを達成することができます

switch(およびifおよびexceptions as control flow)に対する主な利点は、コンパイル時に安全であることです。デフォルトのハンドラーがないか、フォールスルーしません。

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

Nugetにあり、net451とnetstandard1.6をターゲットにしています

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.