web-dev-qa-db-ja.com

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

私は最近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を繰り返し使用した場合や、複合三項条件式(非自明な式の場合は非常に煩雑になります-かっこがいっぱい)よりもはるかに明確です。また、キャストのlotを回避し、より具体的な一致、たとえばInRange( ...)VB Select ... Case "x To y"の使用に相当する一致。

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

さらに、上記の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#言語サポートの可能性について...期待しています;-p)、あらゆる意見/入力/批評/などを歓迎します。

148
Marc Gravell

私はそれが古いトピックであることを知っていますが、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));
}
22
Marcus Pierce

Bart De Smetの excellent blog には、あなたが説明したとおりのことを行うための8部構成のシリーズがあります。最初の部分を見つけます here

82
mancaus

C#でこのような「機能的な」ことを試みた後(さらに本を試してみても)、いくつかの例外を除いて、そのようなことはあまり役に立たないという結論に達しました。

主な理由は、F#などの言語がこれらの機能を真にサポートすることで多くの力を得ているからです。 「できる」ではなく、「簡単で、明確で、期待されている」。

たとえば、パターンマッチングでは、不完全な一致がある場合、または別の一致がヒットしない場合をコンパイラに通知します。これは、オープンエンド型ではあまり有用ではありませんが、識別されたユニオンまたはタプルに一致する場合、非常に便利です。 F#では、人々がパターンマッチすることを期待し、それは即座に意味をなします。

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

私がC#で頻繁に(プロジェクト間で)使用することになったもの:

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

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

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

他のリンク:

37
MichaelGG

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

そうは言っても、F#やHaskellのようなこのタイプの機能を持つマルチパラダイムおよび関数型言語で遊んで少し時間を費やしたことがあります。スイッチオンする必要がある型を記述していないため、仮想メソッドを実装することはできません)、差別化されたユニオンとともに言語に歓迎します。

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

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

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

25
Greg Beech

これらの種類のライブラリ(言語拡張機能のように振る舞う)は広く受け入れられるとは思わないが、それらは遊ぶのが楽しく、特定のドメインでこれが役立つ小さなチームに本当に役立つ可能性がある。たとえば、このようなものやその他の任意のタイプのテストを行う「ビジネスルール/ロジック」を大量に作成している場合、それがどのように役立つかがわかります。

これが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
22
Brian

あなたの質問に答えるには、はい、パターンマッチの構文構造が便利だと思います。私にとっては、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));
        }
    }
13
cdiggins

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

マルチパラダイム言語の設計には間違いがありませんが、反対に、C#にラムダを含めることは非常に良いことです。また、Haskellは、たとえばIO。しかし、それは非常にエレガントなソリューションではなく、Haskellのやり方ではありません。

しかし、シーケンシャルプロシージャプログラミング言語はラムダ計算の観点から理解でき、C#はシーケンシャルプロシージャ言語のパラメータ内にうまく収まるので、ぴったりです。しかし、たとえばHaskellの純粋な機能コンテキストから何かを取り出して、その機能を純粋ではない言語に入れても、それだけを行っても、より良い結果は保証されません。

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

9
John Leidegren

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

5
bacila

タイプを切り替えるのはあまり「Cシャーピー」ではありませんが、一般的な使用では構造がかなり役立つことを知っています-それを使用できる個人プロジェクトが少なくとも1つあります(管理可能なATMです)式ツリーの再書き込みに関して、コンパイルのパフォーマンスに問題はありますか?

4
Simon Buchan

これは本当に面白い(+1)に見えると思いますが、注意が必要なことが1つあります。C#コンパイラはswitchステートメントの最適化が非常に得意です。短絡だけでなく、あなたが持っているケースの数などによって完全に異なるILが得られます。

あなたの特定の例は、私が非常に便利だと思うことをします-typeof(Motorcycle)は定数ではないため、タイプごとの構文に相当する構文はありません。

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

3
Keith

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をターゲットとしています。

0
mcintyre321