Is there any benefit to this switch / pattern matching idea?
Asked Answered
W

10

160

I've been looking at F# recently, and while I'm not likely to leap the fence any time soon, it definitely highlights some areas where C# (or library support) could make life easier.

In particular, I'm thinking about the pattern matching capability of F#, which allows a very rich syntax - much more expressive than the current switch/conditional C# equivalents. I won't try to give a direct example (my F# isn't up to it), but in short it allows:

  • match by type (with full-coverage checking for discriminated unions) [note this also infers the type for the bound variable, giving member access etc]
  • match by predicate
  • combinations of the above (and possibly some other scenarios I'm not aware of)

While it would be lovely for C# to eventually borrow [ahem] some of this richness, in the interim I've been looking at what can be done at runtime - for example, it is fairly easy to knock together some objects to allow:

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

where getRentPrice is a Func<Vehicle,int>.

[note - maybe Switch/Case here is the wrong terms... but it shows the idea]

To me, this is a lot clearer than the equivalent using repeated if/else, or a composite ternary conditional (which gets very messy for non-trivial expressions - brackets galore). It also avoids a lot of casting, and allows for simple extension (either directly or via extension methods) to more-specific matches, for example an InRange(...) match comparable to the VB Select...Case "x To y" usage.

I'm just trying to gauge if people think there is much benefit from constructs like the above (in the absence of language support)?

Note additionally that I've been playing with 3 variants of the above:

  • a Func<TSource,TValue> version for evaluation - comparable to composite ternary conditional statements
  • an Action<TSource> version - comparable to if/else if/else if/else if/else
  • an Expression<Func<TSource,TValue>> version - as the first, but usable by arbitrary LINQ providers

Additionally, using the Expression-based version enables Expression-tree re-writing, essentially inlining all the branches into a single composite conditional Expression, rather than using repeated invocation. I haven't checked recently, but in some early Entity Framework builds I seem to recall this being necessary, as it didn't like InvocationExpression very much. It also allows more efficient usage with LINQ-to-Objects, since it avoids repeated delegate invocations - tests show a match like the above (using the Expression form) performing at the same speed [marginally quicker, in fact] compared to the equivalent C# composite conditional statement. For completeness, the Func<...> based-version took 4 times as long as the C# conditional statement, but is still very quick and is unlikely to be a major bottleneck in most use-cases.

I welcome any thoughts / input / critique / etc on the above (or on the possibilities of richer C# language support... here's hoping ;-p).

Warmongering answered 1/10, 2008 at 6:34 Comment(9)
"I'm just trying to gauge if people think there is much benefit from constructs like the above (in the absence of language support)?" IMHO, yes. Doesn't something similar already exist? If not, feel encouraged to write a lightweight library.Weikert
You could use VB .NET which supports this in its select case statement. Eek!Subulate
I will also toot my own horn and add a link to my library: functional-dotnetSurbase
I like this idea and it makes for a very nice and much more flexible form of a switch-case; however, isn't this really an embellished way of using Linq-like syntax as an if-then wrapper? I would discourage someone from using this in place of the real deal, i.e. a switch-case statement. Don't get me wrong, I think it has it's place and I will probably look for a way to implement.Lordan
If you write something more than a hello-world with Active Patterns (F# 3.0), I think, that will push you over the edge...Headman
@modosansreves push you over the edge into using F# for this, or into never wanting to use Active Patterns again? Unclear what you are implyingKedron
@Kedron currently Marc just looks at F#, so, I was thinking "over the edge" is praising F# in this context; yet I admit, it's a vague statement.Headman
Although this question is over two years old, it feels pertinent to mention that C# 7 is coming out soon(ish) with pattern matching capabilities.Sperling
The C# 7 pattern matching doesn't replicate F#'s exhaustive matching.Countershaft
B
23

In C# 7, you can do:

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));
}
Bluebird answered 13/12, 2017 at 14:18 Comment(1)
The notable difference here between C# and F# is completeness of the pattern match. That the pattern match covers every possible case available, fully described, warnings from the compiler if you do not. While you can rightfully argue that the default case does this, it also is often in practice a run-time exception.Sounding
C
38

After trying to do such "functional" things in C# (and even attempting a book on it), I've come to the conclusion that no, with a few exceptions, such things don't help too much.

The main reason is that languages such as F# get a lot of their power from truly supporting these features. Not "you can do it", but "it's simple, it's clear, it's expected".

For instance, in pattern matching, you get the compiler telling you if there's an incomplete match or when another match will never be hit. This is less useful with open ended types, but when matching a discriminated union or tuples, it's very nifty. In F#, you expect people to pattern match, and it instantly makes sense.

The "problem" is that once you start using some functional concepts, it's natural to want to continue. However, leveraging tuples, functions, partial method application and currying, pattern matching, nested functions, generics, monad support, etc. in C# gets very ugly, very quickly. It's fun, and some very smart people have done some very cool things in C#, but actually using it feels heavy.

What I have ended up using often (across-projects) in C#:

  • Sequence functions, via extension methods for IEnumerable. Things like ForEach or Process ("Apply"? -- do an action on a sequence item as it's enumerated) fit in because C# syntax supports it well.
  • Abstracting common statement patterns. Complicated try/catch/finally blocks or other involved (often heavily generic) code blocks. Extending LINQ-to-SQL fits in here too.
  • Tuples, to some extent.

** But do note: The lack of automatic generalization and type inference really hinder the use of even these features. **

All this said, as someone else mentioned, on a small team, for a specific purpose, yes, perhaps they can help if you're stuck with C#. But in my experience, they usually felt like more hassle than they were worth - YMMV.

Some other links:

Columbian answered 12/10, 2008 at 11:38 Comment(0)
B
25

Arguably the reason that C# doesn't make it simple to switch on type is because it is primarily an object-oriented language, and the 'correct' way to do this in object-oriented terms would be to define a GetRentPrice method on Vehicle and override it in derived classes.

That said, I've spent a bit of time playing with multi-paradigm and functional languages like F# and Haskell which have this type of capability, and I've come across a number of places where it would be useful before (e.g. when you are not writing the types you need to switch on so you cannot implement a virtual method on them) and it's something I'd welcome into the language along with discriminated unions.

[Edit: Removed part about performance as Marc indicated it could be short-circuited]

Another potential problem is a usability one - it's clear from the final call what happens if the match fails to meet any conditions, but what is the behaviour if it matches two or more conditions? Should it throw an exception? Should it return the first or the last match?

A way I tend to use to solve this kind of problem is to use a dictionary field with the type as the key and the lambda as the value, which is pretty terse to construct using object initializer syntax; however, this only accounts for the concrete type and doesn't allow additional predicates so may not be suitable for more complex cases. [Side note - if you look at the output of the C# compiler it frequently converts switch statements to dictionary-based jump tables, so there doesn't appear to be a good reason it couldn't support switching on types]

Beetlebrowed answered 1/10, 2008 at 7:59 Comment(2)
Actually - the version I have does short circuit in both the delegate and expression versions. The expression version compiles to a compound conditional; the delegate version is simply a set of predicates and func/actions - once it has a match it stops.Warmongering
Interesting - from a cursory look I assumed it would have to perform at least basic checking of each condition as it looked like a method chain, but now I realise the methods are actually chaining an object instance to build it so you could do this. I'll edit my answer to remove that statement.Beetlebrowed
S
23

I don't think these sorts of libraries (which act like language extensions) are likely to gain wide acceptance, but they are fun to play with, and can be really useful for small teams working in specific domains where this is useful. For instance, if you are writing tons of 'business rules/logic' that does arbitrary type tests like this and whatnot, I can see how it would be handy.

I've no clue if this is ever likely to be a C# language feature (seems doubtful, but who can see the future?).

For reference, the corresponding F# is approximately:

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"

assuming you'd defined a class hierarchy along the lines of

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
Semeiology answered 1/10, 2008 at 7:26 Comment(1)
Thanks for the F# version. I guess I like the way F# handles this, but I'm not sure that (overall) F# is the right choice at the moment, so I'm having to walk that middle ground...Warmongering
B
23

In C# 7, you can do:

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));
}
Bluebird answered 13/12, 2017 at 14:18 Comment(1)
The notable difference here between C# and F# is completeness of the pattern match. That the pattern match covers every possible case available, fully described, warnings from the compiler if you do not. While you can rightfully argue that the default case does this, it also is often in practice a run-time exception.Sounding
P
13

Yes I think pattern matching syntactic constructs are useful. I for one would like to see syntactic support in C# for it.

Here is my implementation of a class that provides (nearly) the same syntax as you describe

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");
    }
}

Here is some test code:

    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));
        }
    }
Patentee answered 4/9, 2011 at 20:22 Comment(0)
E
9

The purpose of pattern matching (as described here) is to deconstruct values according to their type specification. However, the concept of a class (or type) in C# doesn't agree with you.

There's nothing wrong with multi-paradigm language design, on the contrary, it's very nice to have lambdas in C#, and Haskell can do imperative stuff to e.g. IO. But it's not a very elegant solution, not in Haskell fashion.

But since sequential procedural programming languages can be understood in terms of lambda calculus, and C# happens to fit well within the parameters of a sequential procedural language, it's a good fit. But, taking something from the pure functional context of, say, Haskell, and then putting that feature into a language which is not pure, well, doing just that will not guarantee a better outcome.

My point is what makes pattern matching tick is tied to the language design and data model. Having said that, I don't believe pattern matching to be an useful feature of C# because it does not solve typical C# problems, nor does it fit well within the imperative programming paradigm.

Eject answered 13/8, 2009 at 7:29 Comment(1)
Maybe. Indeed, I would struggle to think of a convincing "killer" argument for why it would be needed (as opposed to "perhaps nice in a few edge cases at the cost of making the language more complex").Warmongering
B
5

In my humble opinion, the object oriented way of doing such things is the Visitor pattern. Your visitor member methods simply act as case constructs and you let the language itself handle the appropriate dispatch without having to "peek" at types.

Byrle answered 16/6, 2009 at 7:5 Comment(0)
I
4

Although it's not very 'C-sharpey' to switch on type, I know that construct would be pretty helpful in general use - I have at least one personal project that could use it (although its managable ATM). Is there much of a compile performance problem, with the expression tree re-writing?

Instruct answered 1/10, 2008 at 7:13 Comment(4)
Not if you cache the object for re-use (which is largely how C# lambda expressions work, except the compiler hides the code). The re-writing definitely improves the compiled performance - however, for regular use (rather than LINQ-to-Something) I expect the delegate version might be more useful.Warmongering
Note also - it isn't necessarily a switch on type - it could also be used as a composite conditional (even thru LINQ) - but without a messy x=> Test ? Result1 : (Test2 ? Result2 : (Test3 ? Result 3 : Result4))Warmongering
Nice to know, although I was meaning the performance of the actual compilation: how long csc.exe takes - I'm not familiar enough with C# to know if that is ever really a problem, but it's a big issue for C++.Instruct
csc won't blink at this - it is so similar to how LINQ works, and the C# 3.0 compiler is quite good at LINQ/extension methods etc.Warmongering
I
3

One thing to be careful of: the C# compiler is pretty good at optimising switch statements. Not just for short circuiting - you get completely different IL depending on how many cases you have and so on.

Your specific example does do something I'd find very useful - there is no syntax equivalent to case by type, as (for instance) typeof(Motorcycle) is not a constant.

This gets more interesting in dynamic application - your logic here could be easily data-driven, giving 'rule-engine' style execution.

Imprecision answered 1/10, 2008 at 9:16 Comment(0)
C
1

You can achieve what you are after by using a library I wrote, called OneOf

The major advantage over switch (and if and exceptions as control flow) is that it is compile-time safe - there is no default handler or fall through

   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
            )
        );

It's on Nuget and targets net451 and netstandard1.6

Countershaft answered 13/9, 2017 at 15:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.