C# 8: Switch expressions
alexatnet.com
alexatnet.com
What's missing is a way to express an anonymous block expression in C#. I'm not sure that's a gap worth filling, though - if the logic is worth a block, isn't it worth a name?
Or actually, to bait a few more hn clicks (and provide a fuller description): "Pattern matching in C# 8 with switch expressions"
The new syntax is not intuitive, not more readable, just a bit more terse.
But, it makes almost complete sense if you instead look at it as coping with Lisp envy. I'm still amazed that they managed to sneak in function literals and FEXPRs (Linq), a metaobject system (dynamic + DLR), a limited form of call/cc (aysnc/await), first-class metaprogramming (Roslyn -- albeit more in the style of Smalltalk rather than Lisp), and a REPL.
I just mean that C# looks messy from the point-of-view of an ML-wannabe, but pretty elegant as an aspiring Lisp. And I say this as someone that is fluent in F# and prefers functional programming in the style of ML rather than that of Lisp.
"Confessions of a Used Programming Language Salesman, Getting the Masses Hooked on Haskell"
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72....
While I use them every day, my favorite is still “replacing” String.Format with string literals. Shorter, sure, but less intuitive particularly for people coming from another language. You can do roughly the same thing in PHP with backticks, but most people avoid them for the same reasons.
I’m not sure how I feel about the trend as a whole. As long as I can continue using the old methods as well I suppose I don’t have much room to complain, but sometimes it does feel like we’re just changing for the sake of change and that’s usually a bad indicator.
Many languages also have format strings with interpolation: JavaScript, Python, Ruby, Swift, PHP, etc.
Java, C/C++/Objective-C/Go (I'm lumping them up because they're basically C + C NextGens), Matlab, SQL, ASM don't have them.
Everything else, has them.
On the list of languages that don't have it, Java is planning to add it, I don't see the C family to change because it's very conservative, and the others are domain specific and don't care that much about string operations.
For example:
var h = "Hello ";
var w = "world";
var output = String.Format("{1} {2}", h, w);
output = $"{h} {w}";
Would compile fine but throw on the String.Format() line due to the typo.There are also instances where, such as with Console.Write you are now formatting a string in order to pass it into... a method that formats strings. Yes, the same benefit is still provided, but with a fairly obvious decrease in clarity and increase in redundancy.
You feel that "{0} {1} {2}" provides better clarity than $"{FirstName} {MiddleInitial} {LastName}"?
In what's ways specifically? Using String.Format you have to keep your indices correct and 'compiling' the string in your head requires jumping back and forth between the format string and the args.
Interpolation wins on every account as far as I'm concerned.
Maybe for people coming from C++, but string interpolation is a feature in Python, JavaScript, PHP, etc. It's not exactly an unusal concept (like it was back when PHP started doing it)
Once you start making switch expressions, the break keyword makes no sense. And with pattern matching the case keyword technically wouldn't be nessesary for normal switch statements already, might as well drop it for the new syntax.
I find this completely reasonable, and I think it's very readable, especially compared to the closest equivalents: putting a single switch in a function and calling that, or using nested tertiary operators:
const sqlOp = op switch {
"&&" => "AND",
"||" => "OR",
_ => throw new NotSupportedException()
}
vs function getSqlOp(string op) {
switch op {
case "&&": return "AND";
case "||": return "OR";
default: throw new NotSupportedException();
}
}
// ... far away
const sqlOp = getSqlOp(op);
vs const sqlOp = op=="&&" ? "AND" : op=="||" ? "OR" : throw new NotSupportedException();In a switch expression of course that is moot since break is a statement, and statements can't appear in expressions.
x: y: DoX()
but you can't have
x: DoX() y: DoY()
at all.
case x:
DoX(); goto case y;
case y:
DoY(); break; case "foo" { /* ... */ }
case "bar" /* ... */;This is a half-assed, syntactically ugly implementation of case statements in functional languages like Elm or Haskell - the point of which is to cover all possible executions of a branch explicitly, to prevent runtime errors.
Also, there is an elegance to the functional style of pattern matching. And C# has been incorporating a lot of functional paradigms for more than a decade now which may initially feel "out of place" but ultimately benefited the language greatly.
Edit: ye, iOS, ye!
"+" => ((Func<int>)(() => {
Log("addition");
return a + b;
}))(),
This is incredibly ugly. Why couldn’t it be “+” => {stuff}What he's doing is creating an anonymous function which takes no parameters and returns an int. That's what a Func<int> is, and then he is executing that function, that's the () bit after }))
He could write it nicer as:
"+" => Add(),
Where he defines add as int Add() => a + b;
He's just doing too much inline, 99% of C# developers would not write code as he has done there.Its' the bloggers code style that is the problem here, not the new switch syntax. Although he probably did it to make the example more self contained and terse.
It's two short lines.
The problem is that he's writing a small but more than one line bit of code, so the overhead of the anonymous function is just as big as the code itself, and arguably more complex.
T Log<T>(string msg, Func<T> func) {
Log(msg);
return func();
}
var result = operation switch {
"+" => Log("addition", () => a + b),
"-" => Log("subtraction", () => a - b),
"/" => Log("division", () => a / b),
_ => throw new NotSupportedException()
};
This is shorter, cleaner, and how most folks would actually write something like this, I imagine. T Log<T>(string msg, T result) {
Log(msg);
return result;
}
var result = operation switch {
"+" => Log("addition", a + b),
"-" => Log("subtraction", a - b),
"/" => Log("division", a / b),
_ => throw new NotSupportedException()
};
You already have a lambda for calling Log(), no need for a second to call the inner function. This does change the order of evaluation though.I just mention it for academic interest. To be honest, I find the idea of passing a parameter to Log() (whether it's the value or the function) that your don't actually want logged, so that you can shoehorn two statements into an expression, abhorrent. Just write the two statements out! I think an old-fashioned switch statement is the right tool here.
You've basically reimplemented Haskell's Debug.Trace[0] :)
The difference is of course that Haskell doesn't really have statements in the same way that C# does, so in Haskell it's nescessary to turn the log/trace-function into an identity function after you've applied the message-string.
I agree that it is ugly, and in any serious project I would use a proper logging setup, but I almost feel like the ugliness is a feature not a bug. It's like an extra cost when doing debug-by-print, which at least seem to keep me more mindful about cleaning up after myself.
[0] http://hackage.haskell.org/package/base-4.12.0.0/docs/Debug-...
For years the language has been trying to evolve from being aesthetic and easy to understand and read.
With the functional implementations and patterns I believe the language is trying to embrace too much.
This is not specific to the swith expression in this context but in general.
You can write C# in too many forms and I don't believe that's really good.
Why not? And what, exactly, defines "too many"?
This is a big problem in the JavaScript world in my opinion. So many different ways to solve common problems that it's difficult to approach. OP might be worried this could be happening to C#.
While a bit snarky I do think it is one of the fundamental problems with programming language innovation. No matter how smart we are, learning is effortful. And we frequently judge languages not by how productive it is, but by how quickly we can start feeling productive in it. Which often preferences familiar concepts over more powerful concepts.
There is a very long feedback loop in properly evaluating a language. At least 1-2 years. You need not just figure out how to do what you already know in the language, you need to learn the new way to think in the language. And that takes time and solving lots of problems. This is only sped up if the language's concepts are kept very similar to a language you already know.
We are the bottleneck that slows down language improvement. There is no way anyone can convince me that the hodgepodge of Algol derived languages is one of the best ways out there of writing code. But most new languages will be similar because the hurdles of trying to teach people anything else alongside the actual new interesting concepts of your language are too much of a blocker for any reasonable adoption.
Yes, that's why we program in Brainf__k here: It only has the bare essentials and it's so easy to learn.
I think Scala was innovating in this respect before Kotlin ate its lunch (it may still be).
You could use Scala as everything between Java++ and Haskell--. Developers proficient in Java++ often had a difficult time understanding code written in Haskell--.
That's been the case for the majority of supposed "new features" in C# for at least the past 8 years or so -- LINQ, inline variables, anonymous functions, string literals etc. It's all just syntactic sugar on top of existing features.
So the real new feature in C# 3.0 are the expression trees. LINQ is only one place where expression trees are used.
I agree with other posters that this introduces a new style, but when I look at C# 2.0 I see a language I do not want to use anymore. It is old and clumsy. Like Java :)
However, when it comes to modern C# and memory management I also think that readability suffers a lot. Memory management on that level is well known to the professional C/C++ folks but most of us are just overloaded with it.
Java's switch still operates only on String, int, short, byte, char, and their wrapper types: https://blog.codefx.org/java/switch-expressions/
// now you kind of enforced to
// handle all values, otherwise
// it will not compile
I fail to see how it will not compile if all values are not handled. C# switch is not traditional pattern matching, because you don't get compile time feedback.Claiming that is does is disingenuous.
> Since an expression needs to either have a value or throw an exception, a switch expression that reaches the end without a match will throw an exception. The compiler does a great job of warning you when this may be the case, but will not force you to end all switch expressions with a catch-all: you may know better!
I haven't used the feature yet myself, so I can't say it definitively - but it sounds like it does provide compile time feedback while still allowing you to compile.
[1]: https://blogs.msdn.microsoft.com/dotnet/2019/01/24/do-more-w...
[1] https://github.com/dotnet/corefx/issues/33284#issuecomment-4...
I wonder, though, at what point a programming language collapses on itself, from too much syntax. I haven't used C# since the 4.x days, and I recall back then it was already on the verge of being too complex for me to fully understand. Admittedly my personal threshold of complexity is lower than most programmers', but this is a really big language. Do they plan to keep adding syntax indefinitely?
At some point, it's going to have to get eclipsed by something else. The next generation will be as effective a tool, for the kinds of things most people want to make, and not too complex for most people to understand. Perhaps part of that would consist of just using expressions for everything from the start.
You'd mark out older syntaxes/APIs as restricted or deprecated and compilations would yield warnings or errors (when those features were used) depending on the compiler level.
This would allow new projects to be developed using only the newer (standard) syntax, while allowing a transitional vector for older projects.
"a rethinking of C#" is a good description of F#.
I'm a huge fan of C#, but the lack of features makes Go so damned pleasant to read. As an extreme example, the new C# record type really breaks the neurons I have dedicated to the language:
public struct Pair(object First, object Second);
As soon as I see parenthesis, it's a method. I want record types, but that syntax is foreign. I wish Anders would spend a few months back on the C# team, he has a great knack for keeping things as consistent and minimal as possible. public struct Pair(object First, object Second);
Scala, Kotlin, Rust among others use the same or similar syntax.I can't say it's complex. If anything, the complexity has been reduced with local type inference and lambdas.
I think you wouldn't be able to tell C# from Javascript these days :)
* I acknowledge my ignorance in thinking that scala was the first one to the table with this.
That's a big part of the story here.
Sometimes you want to provide one of two different values based on a condition. If you use if/else, then each statement body has to contain either an assignment (if the value is to be used later in the same function), or a return (if the value is to be immediately returned from the enclosing function). It is often more clear and concise to use a ?: expression at the site where the value is to be used.
Now consider the case where you need to choose among three or more different values. You can use chained if/else statements, which have the same drawback of needing to contain either assignments or returns. Or you can use chained ?: expressions, but there's really no good way to format this in a way that remains clean and easy to understand.
The new switch expression provides a cleaner alternative to chained ?: expressions.
Uhmmmm....I'm a professional C++ programmer who uses switch statements quite a lot and no, I couldn't instinctively tell what it was doing straight away. Am I being dumb?