c# - 分解 - パターンマッチング方式 画像処理




スイッチ/パターンマッチングアイデア (8)

Bart De Smetの優れたブログには、あなたが描いたことを正確に行うことに関する8つのシリーズがあります。 here最初の部分を見つけてhere

私は最近、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、または複合三項条件を使用した場合よりもはるかに明確です(非自明な式では非常に乱雑です - 大括弧)。 また、 多くのキャストを避けることができ、VB Selectに匹敵するInRange(...)の一致など、より具体的な一致への簡単な拡張(直接または拡張メソッド経由)が可能です。Case "x To y " 使用法。

私はちょうど人々が上記(言語サポートがない場合)のような構造から多くの利益があると思っているかどうかを判断しようとしていますか?

さらに、私は上記の3つの変種で遊んでいることに注意してください:

  • 評価のためのFunc <TSource、TValue>バージョン - 複合三元条件文に匹敵する
  • Action <TSource>バージョン - if / else if / elseと似ていますif / else if / else
  • Expression <Func <TSource、TValue >>バージョン - 最初のものですが、任意のLINQプロバイダが使用できます

さらに、Expressionベースのバージョンを使用すると、繰り返しの呼び出しを使用するのではなく、すべてのブランチを基本的に単一の合成条件式にインライン展開するExpression-Treeの再書き込みが可能になります。 私は最近チェックしていませんが、Entity Framework初期のいくつかでは、InvocationExpressionがあまり好きではないので、これが必要であることを思い出すようです。 また、繰り返しのデリゲート呼び出しを避けるため、LINQ-to-Objectsでの効率的な使用が可能になります。テストでは、同等のC#と比較して同じ速度([少し速い]複合条件文。 完全性を期すために、FuncのベースバージョンはC#の条件文の4倍の長さを要しましたが、まだ非常に速く、ほとんどのユースケースで大きなボトルネックになる可能性は低いです。

上記の(またはより豊かなC#言語サポートの可能性については...ここで期待している;-))に関する考え/入力/批評/などを歓迎します。


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

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

たとえば、パターンマッチングでは、不完全なマッチがあるかどうか、または別のマッチが決してヒットしないことをコンパイラに伝えます。 これは、オープン・エンド型ではあまり役に立ちませんが、識別された共用体またはタプルと一致するときは非常に面白いです。 F#では、人々はパターンマッチを期待し、それはすぐに意味があります。

「問題」は、いったん機能概念を使い始めると、それを継続したいのは自然なことです。 しかし、タプル、関数、部分メソッドの適用とカリング、パターンマッチング、入れ子関数、ジェネリックス、モナドサポートなどをC#で非常に醜く、非常に迅速に取得します。 それは楽しいですし、C#では非常に賢い人たちがいくつかの非常にクールなことをしていますが、実際にはそれを重視しています。

私が何度も(プロジェクト間で)C#で頻繁に使用するようになったのは:

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

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

これは、他の誰かが述べたように、小規模なチームで、特定の目的のために、はい、あなたがC#で立ち往生している場合に役立つかもしれません。 しかし、私の経験では、彼らは通常、彼らが価値あるものよりももっと面倒なように感じました - YMMV。

その他のリンク:


あなたの質問に答えるために、私は構文マッチング構文が有用であると思います。 私はそれのために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));
        }
    }

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

つまり、私はこのタイプの能力を持つF#やHaskellのようなマルチパラダイムや関数型言語で遊ぶのに少し時間を費やしていました。以前は便利だったいくつかの場所に出くわしました。あなたがスイッチする必要があるタイプを書いていないので、仮想メソッドを実装することはできません)、それは差別化された組合と一緒に言語に歓迎するものです。

[編集:Marcが短絡している可能性があると表示されているため、パフォーマンスについての部分を削除しました]

もう1つの潜在的な問題は、使い勝手の問題です。最終的なコールからは、条件が一致しないとどうなるかはっきりしていますが、2つ以上の条件に一致した場合の動作は何ですか? それは例外をスローする必要がありますか? 最初の試合または最後の試合を返すべきでしょうか?

私がこの種の問題を解決するために使用する方法は、キーとして型を持つ辞書フィールドを使用し、値としてラムダを使用することです。これは、オブジェクトイニシャライザ構文を使用して構築するのがかなり簡潔です。 ただし、これは具体的な型を説明し、追加の述部を許可しないため、より複雑な場合には適していない可能性があります。 [補足 - C#コンパイラの出力を見れば、頻繁にswitch文が辞書ベースのジャンプテーブルに変換されるため、タイプの切り替えをサポートできないという十分な理由はないようです]


パターンマッチング( here説明したようhere )、その目的は型の指定に従って値を分解することです。 しかし、C#のクラス(または型)のコンセプトはあなたに同意しません。

逆に、マルチパラダイム言語の設計には間違いがあります。逆に、C#でlambdasを使用することは非常にうれしく、HaskellはIOなどに不可欠なことを行うことができます。 しかし、それは非常にエレガントなソリューションではなく、Haskellのファッションではありません。

しかし、逐次手続き型プログラミング言語はラムダ計算で理解でき、C#は逐次手続き型言語のパラメータ内でうまく収まるので、それは良い選択です。 しかし、ハスケルの純粋な機能コンテキストから何かを取り出し、その機能を純粋ではない言語に入れることは、まあまあ、それだけでは、よりよい結果を保証するものではありません。

私の指摘はこれです。パターンマッチングのティックは、言語デザインとデータモデルに結びついています。 しかし、パターンマッチングはC#の便利な機能であるとは信じられません。なぜなら、典型的なC#の問題を解決したり、命令型プログラミングのパラダイムには適していないからです。


私が書いた、 OneOfというライブラリを使って、あなたが何をしているのかを達成することができます

switch (およびexceptions as control flow ifexceptions 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をターゲットにしています


私はこれらの種類のライブラリ(言語拡張のような役割を果たす)は広く受け入れられるとは思っていませんが、遊ぶのが楽しく、これが役に立つ特定のドメインで働く小さなチームにとっては本当に便利です。 たとえば、このような任意のタイプのテストを行う 'ビジネスルール/ロジック'をたくさん書く場合、どのように役立つかがわかります。

私はこれが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

私はそれが古い話題だと知っていますが、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));
}




switch-statement