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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) | |
} |
C#:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); } | |
} | |
} |
We can transform C# code into following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
); | |
} |
Which isn’t perfect but is a whole lot nicer.