Simulating Algebraic Data Types (AKA tagged unions) and pattern matching them in C#

After programming for a bit in Scala I’ve fallen in love with functional programming. And given how concise the code is there’s not much to hate in it.

One of the fine features of Scala is an easy way of defining ADT’s.

Here’s a comparision of defining same ADT with C# and Scala:

public abstract class Kind {
public class Global : Kind {}
public class World : Kind {
public readonly int world;
public World(int world) {
this.world = world;
}
}
public class Level : Kind {
public readonly int world;
public readonly int level;
public readonly bool bonus;
public Level(int world, int level, bool bonus) {
this.world = world;
this.level = level;
this.bonus = bonus;
}
}
}
view raw ADT.cs hosted with ❤ by GitHub
sealed trait Kind
object Kind {
case object Global extends Kind
case class World(world: Int) extends Kind
case class Level(world: Int, level: Int, bonus: Boolean) extends Kind
}
view raw ADT.scala hosted with ❤ by GitHub

Scala is a lot more concise, but that’s just the way C# is – noisy.

Now lets take a look on traditional approach of matching those.

Scala:

private def getLeaderboardName(kind: Kind, lbKind: LeaderboardKind) = kind match {
case kg @ Kind.Global =>
LeaderboardName(s"lb_${leaderboardKindToString(lbKind)}", lbKind)
case Kind.World(w) =>
LeaderboardName(s"lb_w${w}_${leaderboardKindToString(lbKind)}", lbKind)
case Kind.Level(w, l, b) =>
LeaderboardName(
s"lb_w${w}_l$l${if (b) "b" else "")}_${leaderboardKindToString(lbKind)}",
lbKind
)
}
view raw Matching.scala hosted with ❤ by GitHub

C#:

private static LeaderboardName getLeaderboardName(
Kind kind, LeaderboardKind lbKind
) {
var kg = kind as Kind.Global;
if (kg != null)
return new LeaderboardName(
string.Format("lb_{0}", leaderboardKindToString(lbKind)), lbKind
);
var kw = kind as Kind.World;
if (kw != null)
return new LeaderboardName(
string.Format("lb_w{0}_{1}", kw.world, leaderboardKindToString(lbKind)),
lbKind
);
var kl = kind as Kind.Level;
if (kl != null)
return new LeaderboardName(
string.Format(
"lb_w{0}_l{1}{2}_{3}", kl.world, kl.level, kl.bonus ? "b" : "",
leaderboardKindToString(lbKind)
), lbKind
);
throw new Exception("Unknown kind " + kind);
}
view raw gistfile1.cs hosted with ❤ by GitHub

Good thing we can at least use some custom code and functional magic to make that similar to our C# version.

Given that we use this:

using System;
namespace Utils.Match {
public interface IMatcher<in Base, Return> where Base : class {
IMatcher<Base, Return> when<T>(Func<T, Return> onMatch)
where T : class, Base;
Return get();
Return getOrElse(Func<Return> elseFunc);
}
public class MatchError : Exception {
public MatchError(string message) : base(message) {}
}
public class Matcher<Base, Return> : IMatcher<Base, Return>
where Base : class {
private readonly Base subject;
public Matcher(Base subject) {
this.subject = subject;
}
public IMatcher<Base, Return> when<T>(Func<T, Return> onMatch)
where T : class, Base {
var casted = subject as T;
if (casted != null)
return new SuccessfulMatcher<Base, Return>(onMatch.Invoke(casted));
return this;
}
public Return get() {
throw new MatchError(string.Format(
"Subject {0} of type {1} couldn't be matched!", subject, typeof(Base)
));
}
public Return getOrElse(Func<Return> elseFunc) { return elseFunc.Invoke(); }
}
public class SuccessfulMatcher<Base, Return> : IMatcher<Base, Return>
where Base : class {
private readonly Return result;
public SuccessfulMatcher(Return result) {
this.result = result;
}
public IMatcher<Base, Return> when<T>(Func<T, Return> onMatch)
where T : class, Base { return this; }
public Return get() { return result; }
public Return getOrElse(Func<Return> elseFunc) { return get(); }
}
public class MatcherBuilder<T> where T : class {
private readonly T subject;
public MatcherBuilder(T subject) {
this.subject = subject;
}
public IMatcher<T, Return> returning<Return>() {
return new Matcher<T, Return>(subject);
}
}
public static class Match {
public static MatcherBuilder<T> match<T>(this T subject)
where T : class { return new MatcherBuilder<T>(subject); }
}
}
view raw Match.cs hosted with ❤ by GitHub

We can transform C# code into following:

private static LeaderboardName getLeaderboardName(
Kind kind, LeaderboardKind lbKind
) {
return new LeaderboardName(
kind.match().returning<string>
.when<Kind.Global>(_ => string.Format(
"lb_{0}", leaderboardKindToString(lbKind)
))
.when<Kind.World>(kw => string.Format(
"lb_w{0}_{1}", kw.world, leaderboardKindToString(lbKind)
))
.when<Kind.Level>(kl => string.Format(
"lb_w{0}_l{1}{2}_{3}", kl.world, kl.level, kl.bonus ? "b" : "",
leaderboardKindToString(lbKind)
))
.get(),
lbKind
);
}
view raw ADTMatch.cs hosted with ❤ by GitHub

Which isn’t perfect but is a whole lot nicer.