views:

3362

answers:

11

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

+2  A: 

I am not really sure what you are trying to do beyond the snippet provided (still early), but I like it. What I understand is that you have a working version, is that correct?

leppie
I have versions working enough to play with for "what if?" scenarios... the delegate version is fairly trivial anyway; the Expression version is more complex.
Marc Gravell
Re the "what" - avoid the if(foo is X) {...} else if (foo is Y and ((Y)foo).SomePredicate) {...} else {...} that tend to ctop up (note the predicate etc which means "virtual" isn't the answer).Have you seen the pattern matching in F#? That explains most of it...
Marc Gravell
I have glanced it, but not used F# really.
leppie
+3  A: 

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?

Simon Buchan
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.
Marc Gravell
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))
Marc Gravell
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++.
Simon Buchan
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.
Marc Gravell
+8  A: 

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
Brian
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...
Marc Gravell
+12  A: 

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]

Greg Beech
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.
Marc Gravell
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.
Greg Beech
+28  A: 

Bart De Smet's excellent blog has an 8 part series on doing exactly what you describe. Find the first part here.

mancaus
+1 for a good link, looks like a lot of other sweet info there :)
leppie
Excellent link - thanks. A pleasingly similar approach too, which is encouraging!
Marc Gravell
+1  A: 

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 tp write a lightweight library.

Konrad Rudolph
+2  A: 

I think this looks really interesting (+1), but 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.

Keith
+10  A: 

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:

MichaelGG
+2  A: 

I will also toot my own horn and add a link to my library: functional-dotnet

Alexey Romanov
+1  A: 

IMHO the OO 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.

bacila
+3  A: 

Pattern matching (as described here), its purpose 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 noting 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 this, 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.

John Leidegren
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").
Marc Gravell