The amazing (and dangerous!) switch expressions in C# 8
I revisited my tentative understanding of the new pattern matching tools in C# 8 when it made rounds on Hacker News today.
Given that C# started out as a general purpose language within the OOP lineage, it's interesting to see it adopt some of the features made popular by the functional community.
What it also does, though, is to place seeds of doubt in the mind of a curious programmer.
What is this functional stuff? How far can I go with it until my peers will start declining my PRs because they can no longer grok my intent from the code?
When given a choice between procedural and object-oriented mindset, I choose the latter. Similarly, when comparing procedural and functional, the latter also wins in my mind. Procedural is evil, error-prone, inelegant, even vulgar.
It gets more complicated if given a choice between object-oriented and functional. In the context of C#, I am predominantly writing OOP code with some functional patterns mixed-in by way of experimentation.
Not being particularly well-versed in the functional idioms, I take the occasional learning forays into them when they make appearances in C#.
Today's post is about the switch expressions.
The new switch expression in C# 8
The switch expression is a very elegant concept that replaces a procedural statement with an expression. So far so good. Or great, actually.
Switching over simple values
Suppose I want to map a numerical score to a textual evaluation. Before, I could write this:
string evaluation;
if (score >= 0)
{
evaluation = "top performer";
}
if (score >= 50)
{
evaluation = "mediocre";
}
else
{
evaluation = "poor" // default;
}
I don't know about you but the word if
always gives me creeps. And what offends me even more is the split between the declaration of initialization of the evaluation
variable. Gross.
Now, I can write this:
var evaluation = score switch
{
var x when (score >= 80) => "top performer",
var x when (score >= 50) => "mediocre",
_ => "poor" // default
};
Better: no ifs and the variable is declared and initialized in a single step.
There's one limitation I do not like. Suppose I have this silly enum:
public enum Colors
{
Red, Yellow, Orange, Blue, Black
}
Let's say I want to match a color to a string of either "warm" or "cool". I would like to be able to do this:
var mood = color switch
{
Red, Yellow, Orange => "warm",
Blue, Black => "cool",
_ => "undefined" // or whatever
}
Unfortunately, that won't compile. According to this helpful SO question, the way to go is:
var mood = color switch
{
var x when ( x is Colors.Red || x is Colors.Yellow || x is Colors.Orange) => "warm",
var x when ( x is Colors.Blue || x is Colors.Black) => "cool",
_ => "undefined" // or whatever
}
Notice the need for a new helper variable x
in the switch expression combined with the when
filter. I can live with it but it's not as elegant as it could've been.
Switching over tuples
I quite like using tuples as throw-away data carriers used in a private context, inside a method. Their support in modern C# is amazing, and yes, you can now switch over them.
This example is adapted from the Microsoft docs. Notice the dependency on LanguageExt.Core, which gives me the wonderful Option<T>
struct.
using System;
using LanguageExt;
namespace PatternMatchingCs
{
public enum Hand
{
Rock, Paper, Scissors
}
public class RockPaperScissors
{
public Option<Hand> Round(Hand first, Hand second)
=> (first, second) switch
{
(Hand.Rock, Hand.Paper) => Hand.Paper,
(Hand.Rock, Hand.Scissors) => Hand.Rock,
(Hand.Paper, Hand.Rock) => Hand.Paper,
(Hand.Paper, Hand.Scissors) => Hand.Scissors,
(Hand.Scissors, Hand.Rock) => Hand.Rock,
(Hand.Scissors, Hand.Paper) => Hand.Scissors,
(_, _) => Option<Hand>.None // to be interpreted as a tie
};
}
}
The enum Hand
has three members and the method takes two arguments on this enum type, and so the number of combinations is still manageable.
I would say that with more enum members and/or more combinations, this feature would quickly cease to be elegant or maintainable. Here, I like it.
Switching over more complex objects
I'll follow with the usual examples as seen in the Microsoft docs, and introduce classes Triangle
, Rectangle
, and Circle
.
class Triangle
{
public double Base { get; }
public double Height { get; }
}
class Rectangle
{
public double Width { get; }
public double Height { get; }
}
class Circle
{
public double Radius {get;}
}
Notice that these are basically DTOs, "dumb" classes without any methods. Data carriers.
Suppose I want to calculate the area. Using the functional way of thinking that decouples data and functionality, the switch expression lets me do this:
double AreaOf(object param)
{
return param switch
{
Triangle t => t.Base * T.Height / 2,
Rectangle r => r.Width * r.Height,
Circle c => c.Radius * c.Radius * Math.PI,
_ => throw new InvalidArgumentException("Unrecognized parameter")
}
}
So far so good? Not really.
The problems
There are two things I don't like about the examples like this one that I see around. (This one is from the official docs on Pattern Matching).
Number one, object param
.
We've gone a long way from untyped variables of the yesteryear, and are they now coming back?
When would I want to write a function that can take an arbitrary object as its parameter but can only process a few specific classes? Never, that's when.
Which brings me to number two, throw new InvalidArgumentException(...)
.
This is a direct consequence of being flexible on the API surface but actually constrained in the implementation.
The function promises to return a double
value when given an argument of an object
but will mostly throw exceptions if you consider the billion possible subclasses of object
that you could give it.
I don't think that throwing exceptions fits the functional paradigm (the functional folks have the concepts of Option or Either instead). It does fit the OOP mindset, but the object param
doesn't. So what is this, really?
Granted, these are only examples of what you could do, not recommendations of what you should do. And I think that the other examples given therein, such as switching over enums, are sensible. If you must use enums, that is.
Third, why decouple the functionality from the object? Is a (possibly static) function operating on "dumb" data better than a "smart" class and if so, when and how?
Before, we were being taught to have an interface like this:
public interface IShape
{
double Area();
}
And then have Circle
, Triangle
, and Rectangle
implement this interface. By way of this useful abstraction, we then get:
public double CalculateSomething(IShape shape)
{
var area = shape.Area();
// do something with it and return the result
}
That way, any object with an area will implement the interface and will be guaranteed to give me its area. No having to inquire about its type at runtime and deal with exceptions when I cannot determine it.
Also, no problems in having obsolete switch expressions that do not contain logic for new types I've added since writing them.
When facing a problem like this, in that my system deals with subjects that have some things in common, my esthetic preference goes toward OOP with its polymorphism, with its interfaces, classes that implement them, decorators, the whole deal.
Elegant but dangerous
In summary, these switch expressions look very elegant when used for "primitive" values, fairly good when used for processing tuples, and somewhat questionable when used to fork behavior based on parameter type.
Their syntax is beautiful and they replace ugly procedural statements with expressions, with all the advantages this implies for code maintainability, testability, and easiness to grok at first read.
When misused, they let a naive programmer think he's using functional concepts when clearly that is not the case, such as when breaking promises of function declarations / API and throwing exceptions when not being able to deal with inputs.
As always, I could be wrong in many ways about this, and will update this post as I learn more about this functionality.
P.S.
If you feel like we'd get along, you can follow me on Twitter, where I document my journey.
Published on
2023-01-04T16:53:22+00:00 by Jason
2023-01-04T18:26:23+00:00 by TomK
2023-01-31T18:04:28+00:00 by Dustin Nieffenegger