
Draft spec for records and pattern-matching in C# - colinramsay
https://roslyn.codeplex.com/discussions/560339
======
Glide
This is some pretty great stuff. Optimizing the null checking pattern will
help a lot. It's a language feature that seems obvious after seeing the if let
x = something as? SomeType in Swift.

I've only been a year away from C# and I was writing 3.5 code at that time.
It's kind of amazing seeing how quickly C# has moved from year to year
compared to it's closest competitors. I had the same experience when I was
looking at web development after a couple years of not really touching it.
It's really nice to just take a step back and realize how quickly these things
are progressing.

~~~
manojlds
Sorry to say, but writing 3.5 (I hope you mean 3.0) code a year ago mean't you
were far behind. I myself feel that C# has not changed a lot in the last 2
years or more. C# 5.0 was released almost exactly 2 years ago, and even that
didn't bring a lot of changes ( The biggest was of course async-await and the
other one the callerinfo attributes, and that's about it.)

~~~
Locke1689
Async didn't change anything for you? It seems pretty big to us.

~~~
ryanjshaw
I used async for a WPF application. That worked out well. I also experimented
with a scraper using async. I dug myself into holes very quickly (memory usage
ballooned, difficult to debug and reason about application state despite the
code being very easy to read, threads were being created all over the place
which annoyed me even though I understood why).

I rewrote the scraper to use BlockingCollections and Take(), with async being
used solely with the HttpClient (which really needs to learn how to play nicer
with async in the face of network errors!). The code was more decoupled, and
while slightly more difficult to follow because of this, far easier to test
and debug because the working set of my application was explicit rather than
hidden in tasks. This had the added benefit of making it easy to persist and
restore state. For me, the concurrent collections introduced in .NET 4 have
been the big winner (no more custom thread pools!).

Use of async needs to be matched carefully with the problem domain, especially
in backend development.

------
DanielBMarkham
Watched a FP demo in Scala last week. As an F# guy, I was curious about the
syntax.

I probably didn't get it, but what I thought I saw was a bunch of cool FP
concepts bolted on top of a pretty long-in-the-tooth OO language. About the
time the guy added the third custom DSL, I decided I would never ever code
like that.

I like C#. I love OO. And I'm crazy about pattern-matching. But why do we
think that combining good things from other places, we always get something
that's better?

Perhaps we should start defining languages by what they _don 't_ do?
Otherwise, everything will do everything -- and that increases cognitive load
on the developers and leads to lots of noob errors by those just starting out.

Could somebody explain to me what we would like to achieve with this?

~~~
Locke1689
Perhaps you'd like to explain why you think pattern matching has any necessary
connection to functional programming? Sure, popularized by ML, but otherwise
I'm not buying it. We already have switch and type-checking statements. Those
are just special cases of pattern matching.

~~~
kyllo
But do your "switch and type-checking statements" allow you to implement a
single polymorphic function and also give you a helpful compiler error when
your switch is non-exhaustive?

~~~
Locke1689
_allow you to implement a single polymorphic function_

What do you mean by this? And which type of polymorphism?

~~~
kyllo
Parametric. In ML-family languages (SML, OCaml, Haskell) you can define a
function that pattern-matches its signature.

For example (taken from the Wikipedia article on SML) I could define an
algebraic datatype "shape" as such:

    
    
        datatype shape
           = Circle   of loc * real      (* center and radius *)
           | Square   of loc * real      (* upper-left corner and side length; axis-aligned *)
           | Triangle of loc * loc * loc (* corners *)
    
    

Then I could define an "area" function that takes this as a parameter and
pattern-matches on its type as such, with a different implementation depending
on the type:

    
    
        fun area (Circle (_, r)) = 3.14 * r * r
           | area (Square (_, s)) = s * s
           | area (Triangle (a, b, c)) = heron (a, b, c) (* call to Heron's formula function *)
    
    

Which is also syntactic sugar for:

    
    
        fun area shape =
           case shape
            of Circle (_, r) => 3.14 * r * r
             | Square (_, s) => s * s
             | Triangle (a, b, c) => heron (a, b, c)
    
    

If I were to implement the function like:

    
    
        fun area shape =
           case shape
            of Circle (_, r) => 3.14 * r * r
             | Square (_, s) => s * s
    
    

and forget to include the Triangle case, I would get a compiler error: "match
nonexhaustive". Furthermore if I were to include a redundant case or an
unreachable case, the compiler would also throw an error informing me of that.
Very useful in preventing runtime errors, and languages like Java or C/C++
simply do not provide this.

~~~
Locke1689
Yes, that is how pattern matching works, and no, it has nothing to do with
parametric polymorphism.

No plans to add extra sugar to immediately switch on the arguments of a
function, but I'm pretty sure that switch(arg) is not terrible overhead.

I've addressed exhaustivity in a different comment.

------
kevingadd
This is a really nice set of C# improvements that fit together to solve a big
problem. I love it. I do have some minor questions, though!

The simplest improvement that confused me at first is the ability to declare a
new inline local to handle an 'out' variable:

int x; foo(out x)

becomes

foo(int x)

This by itself is a quality-of-life improvement but it enables the really nice
extraction of values for pattern matching. I'm happy to see this as a general
language feature instead of a hack specific for records. (Was this already
introduced in a proposed future C# version, and I missed it?)

The record classes considerably simplify the creation of simple data
structures, trees, etc. This alone would kill thousands and thousands of lines
of pointless duplicate code in JSIL, so I'm really happy to see it. Are the
properties of record class instances read-only? (It's hard to tell from the
draft spec. I'd be okay with mutable-by-default with opt-in readonly, but
always-readonly would also be okay with me as a simpler design.)

The introduction of an overloaded 'is' operator that accepts user-defined
arguments on the right-hand-side. You define the operator on a given type and
give it out-parameters (and presumably it can accept value parameters, as
well, so you can do more sophisticated matching), and the compiler converts
that into a call to the matching function that, if successful, yields values
from the match. Really clever way to introduce this - the syntax is slightly
confusing, but it avoids introducing new keywords and almost all of this
feature follows nicely from existing C# features.

And switch statements are expanded to be able to pattern-match! Lovely! This
is a natural improvement given that switch statements are already compile-time
sugar for dictionary lookups when you're switch()ing on non-integer values.

It seems like all of this is compile-time sugar that doesn't require any
runtime changes. Is that true? If so, what version of the runtime do you think
will be the minimum for Roslyn output that uses these features?

------
platz
Are the matches closed, enabling errors or warnings, when all the cases aren't
matched on?

Otherwise it just seems like some sugar on switch.

Also, guarded matches would be nice.

~~~
Locke1689
This is in "open questions." Even without language support requiring
exhaustion, this would be a pretty obvious Roslyn custom diagnostic to be
introduced.

~~~
louthy
I think if it could be switched between a compile time error/warning then we'd
be in a great place. I think even in Haskell you need to switch it on as a
compiler option.

------
kodablah
Appears similar to Scala w/ case classes, but I wish it was more vague and
used extractors similar to Scala's unapply. It would make it much more
amenable to existing classes.

~~~
Locke1689
One thing we could do here is expand the definition of extension methods to
include extension operators. I think this makes sense for other reasons.

If we do that, you could provide pattern matching for existing classes, even
ones from metadata, by simply manually specifying an extension is-operator for
the class.

------
keyle
It's interesting that I'm learning Groovy at the moment and I seem to see a
lot of Groovy goodies in this draft. such as
any.of?.these?.could?.be?.nullhey() or the record class very close to Lua.

~~~
manojlds
Those kind of constructs have been there in many languages. Mostly, C# can be
said to borrow stuff from its cousin F#.

------
lindbergh
In the meanwhile, here's a little class I made that implemented just enough
pattern matching as a fluent interface :
[https://gist.github.com/neuschwanstein/6c0c9256c1dc5b370011](https://gist.github.com/neuschwanstein/6c0c9256c1dc5b370011).

Not pretty, but it works well enough for a pragmatic programmer.

~~~
Strilanc
One way to do very case specific pattern matching in C# is to write an
extension method that takes a lambda for each case. For example, my maybe type
for C# has `Match`:

    
    
        may.Match(
            () => print("None"),
            val => print("Some: " + val))
    

Not very flexible, or performant, but covers the basics of common cases.

~~~
munificent
I came up with some similar and wrote a long blog post[1] about it several
years ago. I never ended up using it in practice because it felt too
syntactically weird, but it was a fun little exercise.

[1]: [http://journal.stuffwithstuff.com/2009/05/13/ml-style-
patter...](http://journal.stuffwithstuff.com/2009/05/13/ml-style-pattern-
matching-in-c/)

~~~
louthy
I created a very similar one to you also, and found the same. It's just a bit
too clunky syntactically. The only time I'd use it would be if I'm using it as
part of an expression where a (switch or series of if) statement would break
the expression up.

As well as a generic extension on Object I started adding bespoke Match
extension methods to a monads library that I've been working on [1] (which
again was all about trying to coerce C# into a more expression based style).

So the Option monad Match method would work like so:

    
    
            Func<int> res = (from x in DoSomething()
                             from y in DoSomethingElse()
                             select x + y)
                            .Match(
                                 Just: v => v * 10,
                                 Nothing: 0
                            );
    

Or, the Either monad:

    
    
            var result =
                (from lhs in Two()
                 from rhs in Two()
                 select lhs + rhs)
                .Match(
                    Right: r => r * 2,
                    Left: l => 0
                );
    

[1] [https://github.com/louthy/csharp-monad](https://github.com/louthy/csharp-
monad)

------
paf31
Presumably this doesn't prevent a developer from creating another "pattern
class" subclassing some superclass in another package, thereby breaking
existing pattern matches?

~~~
Locke1689
Mark all your types sealed. Roslyn can have a custom diagnostic analyzer to
yell about unsealed pattern-matched public classes, but I'm not sure if we
want to add language support for a closed set yet.

Scala allows you to seal a type hierarchy to a single file. That would work,
but it seems hacky -- a file feels more like an implementation detail than a
language construct to me. For one, the C# spec doesn't mention files at all --
from its perspective all your code may as well be in one giant file.

~~~
sparkie
C# doesn't allow you to specify both abstract and sealed on the same class -
something that would be really useful for implementing closed types. The
restriction is artificial and unnecessary though - CIL allows the two to
coexist, and it is how the F# compiler implements discriminated unions. I
think it would be wise to aim for feature-compatibility with F#, to ensure you
can pattern match over types defined in F# from C#, and vice-versa.

~~~
Locke1689
You're right of course -- I misspoke. Marking the types _internal_ , not
sealed, would allow the compiler to guarantee that all it sees is all there
is.

