Make It Feel Like Syntax
I chose my career wisely. I can tell because at least once a week I run across a nifty small thing that makes programming feel new again. It’s not that I’m a sucker for novelty, it’s more that when I see something particular clever that makes me think, It really wakes up my brain. Today’s nifty thing is an implementation of switch.
I ran across this entry in Jared Par’s blog. In it he describes a pattern to allow something that feels like a switch, but on the object Type, which won’t go into a switch.
In his code he writes about this pattern:
Type t = sender.GetType();
if (t == typeof(Button)) {
var realObj = (Button)sender;
// action
}
else if (t == typeof(CheckBox)) {
var realObj = (CheckBox)sender;
// action
}
else {
// action
}
If you remember from my previous entry on is, as, and casting, I prefer this pattern:
if ((Button b = sender as Button) != null) {
// do something
}
else if ((CheckBox cb = sender as CheckBox) != null) {
// do something
}
else {
}
He instead provides a static class that lets you rewrite the same code as this:
TypeSwitch.Do(
sender,
TypeSwitch.Case<Button>(() => /* action */),
TypeSwitch.Case<CheckBox>(x => /* action */),
TypeSwitch.Default(() => /* action */));
which I like a lot – it feels a lot like a switch statement, just without the syntax coloring. I looked over his implementation, and thought that maybe we could make it more generic, leverage more of the existing .NET toolset and create an even better tool.
My implementation is fully generic and looks like this in usage:
Switch<Type>.Do(
foo.GetType(),
Switch<Type>.Case(typeof(string),
x => Console.WriteLine("it's a string!")),
Switch<Type>.Case(typeof(int),
x => Console.WriteLine("it's an int!")),
Switch<Type>.Default(x => Console.WriteLine("I don't know!")));
To do this, I abstracted away the whole notion of a switch statement to the following:
A switch statement is an ordered list of predicates with associated actions that are evaluated in sequence. When a predicate returns true, the associated action will be taken and no further work is done.
This differs from C/C#/Java in the sense that those languages have no explicit order to the predicates, and the predicate itself is implicit. We’ll see that we don’t much by enforcing order and we potentially gain a lot from it. In essence, this is much more like the (cond …) statement in lisp/Scheme.
Let’s start with a couple of utility delegates:
public delegate void Action(T x);
public delegate bool Predicate(T x);
A predicate is method that takes one argument are returns true or false. An action is a method of one argument that returns nothing.
From that, I define a static method called Do, which has similar semantics to Jareds:
public static void Do(T x, params KeyValuePair<Predicate, Action>[] cases)
{
KeyValuePair<Predicate, Action> theCase = cases.FirstOrDefault(c => c.Key(x));
if (theCase.Key != null && theCase.Value != null)
theCase.Value(x);
}
Do takes a generic value, x, and a list of predicate/action pairs and applies FirstOrDefault to the list, redirecting the Func to the Predicate. Since KeyValuePair is a struct, the default will be a real struct, just with null in Key and Value.
From there, I added sugar methods that give you various flavors of Case as well as a Default. Here’s the entire class for reference:
static class Switch<T>
{
public delegate void Action(T x);
public delegate bool Predicate(T x);
public static void Do(T x, params KeyValuePair<Predicate, Action>[] cases)
{
KeyValuePair<Predicate, Action> theCase = cases.FirstOrDefault(c => c.Key(x));
if (theCase.Key != null && theCase.Value != null)
theCase.Value(x);
}
public static Action DoNothing = x => { } ;
public static KeyValuePair<Predicate, Action> Case(Predicate predicate)
{
return new KeyValuePair<Predicate, Action>(predicate, DoNothing);
}
public static KeyValuePair<Predicate, Action> Case(T toMatch, Action action)
{
return new KeyValuePair<Predicate, Action>(x => x.Equals(toMatch), action);
}
public static KeyValuePair<Predicate, Action> Case(Predicate predicate, Action action)
{
return new KeyValuePair<Predicate, Action>(predicate, action);
}
public static KeyValuePair<Predicate, Action> Default(Action action)
{
return new KeyValuePair<Predicate, Action>(x => true, action);
}
}
I provide a static Action called DoNothing which is just as exciting as it sounds. There are three flavors of Case that handle when you have a Predicate with no Action, a matchable value with an Action, and a Predicate and Action. I suppose I could also put in a matchable value with no Action, but that’s why I have DoNothing. Finally there is a default, which is really a Case with no predicate, so I use the trivial true predicate.
We now has an equivalent Switch class which is fully generic to the type you want to use. Critically speaking though, it feels a little funny in the use case above. The preponderance of Switch<Type> feels a little clunky, so the question is, can we sugar it up even more? Sure:
static class TypeSwitch
{
public static void Do(object o, params KeyValuePair<Switch<Type>.Predicate, Switch<Type>.Action>[] predicates)
{
Switch<Type>.Do(o.GetType(), predicates);
}
public static KeyValuePair<Switch<Type>.Predicate, Switch<Type>.Action> Case<T>(Switch<Type>.Action action)
{
return new KeyValuePair<Switch<Type>.Predicate, Switch<Type>.Action>(x => x == typeof(T), action);
}
}
This is a basic reimplementation of Jared’s TypeSwitch but in terms of Switch<T>. Now I can rewrite my previous example like this:
TypeSwitch.Do(foo,
TypeSwitch.Case<string>(x => Console.WriteLine("it's a string!")),
TypeSwitch.Case<int>(x => Console.WriteLine("it's an int!")),
Switch<Type>.Default(x => Console.WriteLine("I don't know!"))
);
now we get more of the sugary feel without the repetition.
So we’ve given up the non-strict ordering of a typical switch. What can we get in return? We can create flavors of Do that use other IEnumerable<T> methods or heck, why not just use full-blown queries rather than FindFirstOrDefault. This makes the semantics of switch much more general. You could do an approximate switch for floating point types. You could write DoAll or DoAllWhenFalse (ie, inverted switch).