A Range kata implementation in C# by Mark Seemann
A port of the corresponding F# code.
This article is an instalment in a short series of articles on the Range kata. In the previous article I made a pass at the kata in F#, using property-based testing with Hedgehog to generate test data.
In the conclusion I mused about the properties I was able to come up with. Is it possible to describe open, closed, and mixed ranges in a way that's less coupled to the implementation? To be honest, I still don't have an answer to that question. Instead, in this article, I describe a straight port of the F# code to C#. There's value in that, too, for people who wonder how to reap the benefits of F# in C#.
The code is available on GitHub.
First property #
Both F# and C# are .NET languages. They run in the same substrate, and are interoperable. While Hedgehog is written in F#, it's possible to consume F# libraries from C#, and vice versa. I've done this multiple times with FsCheck, but I admit to never having tried it with Hedgehog.
If you want to try property-based testing in C#, a third alternative is available: CsCheck. It's written in C# and is more idiomatic in that context. While I sometimes still use FsCheck from C#, I often choose CsCheck for didactic reasons.
The first property I wrote was a direct port of the idea of the first property I wrote in F#:
[Fact] public void ClosedRangeContainsList() { (from xs in Gen.Short.Enumerable.Nonempty let min = xs.Min() let max = xs.Max() select (xs, min, max)) .Sample(t => { var sut = new Range<short>( new ClosedEndpoint<short>(t.min), new ClosedEndpoint<short>(t.max)); var actual = sut.Contains(t.xs); Assert.True(actual, $"Expected {t.xs} to be contained in {sut}."); }); }
This test (or property, if you will) uses a technique that I often use with property-based testing. I'm still searching for a catchy name for this, but here we may call it something like reverse test-case assembly. My goal is to test a predicate, and this particular property should verify that for a given Equivalence Class, the predicate is always true.
While we may think of an Equivalence Class as a set from which we pick test cases, I don't actually have a full enumeration of such a set. I can't have that, since that set is infinitely big. Instead of randomly picking values from a set that I can't fully populate, I instead carefully pick test case values in such a way that they would all belong to the same set partition (Equivalence Class).
The test name suggests the test case: I'd like to verify that given I have a closed range, when I ask it whether a list within that range is contained, then the answer is true. How do I pick such a test case?
I do it in reverse. You can say that the sampling is the dual of the test. I start with a list (xs
) and only then do I create a range that contains it. Since the first test case is for a closed range, the min
and max
values are sufficient to define such a range.
How do I pass that property?
Degenerately, as is often the case with TDD beginnings:
public bool Contains(IEnumerable<T> candidates) { return true; }
Even though the ClosedRangeContainsList
property effectively executes a hundred test cases, the Devil's Advocate can easily ignore that and instead return hard-coded true
.
Endpoint sum type #
I'm not going to bore you with the remaining properties. The repository is available on GitHub if you're interested in those details.
If you've programmed in F# for some time, you typically miss algebraic data types when forced to return to C#. A language like C# does have product types, but lack native sum types. Even so, not all is lost. I've previously demonstrated that you can employ the Visitor pattern to encode a sum type. Another option is to use Church encoding, which I've decided to do here.
When choosing between Church encoding and the Visitor pattern, Visitor is more object-oriented (after all, it's an original GoF design pattern), but Church encoding has fewer moving parts. Since I was just doing an exercise, I went for the simpler implementation.
An Endpoint
object should allow one of two cases: Open
or Closed
. To avoid primitive obsession I gave the class a private
constructor:
public sealed class Endpoint<T> { private readonly T value; private readonly bool isClosed; private Endpoint(T value, bool isClosed) { this.value = value; this.isClosed = isClosed; }
Since the constructor is private
you need another way to create Endpoint
objects. Two factory methods provide that affordance:
public static Endpoint<T> Closed<T>(T value) { return Endpoint<T>.Closed(value); } public static Endpoint<T> Open<T>(T value) { return Endpoint<T>.Open(value); }
The heart of the Church encoding is the Match
method:
public TResult Match<TResult>( Func<T, TResult> whenClosed, Func<T, TResult> whenOpen) { if (isClosed) return whenClosed(value); else return whenOpen(value); }
Such an API is an example of poka-yoke because it obliges you to deal with both cases. The compiler will keep you honest: Did you remember to deal with both the open and the closed case? When calling the Match
method, you must supply both arguments, or your code doesn't compile. Make illegal states unrepresentable.
Containment #
With the Endpoint
class in place, you can implement a Range
class.
public sealed class Range<T> where T : IComparable<T>
It made sense to me to constrain the T
type argument to IComparable<T>
, although it's possible that I could have deferred that constraint to the actual Contains
method, like I did with my Haskell implementation.
A Range
holds two Endpoint
values:
public Range(Endpoint<T> min, Endpoint<T> max) { this.min = min; this.max = max; }
The Contains
method makes use of the built-in All method, using a private
helper function as the predicate:
private bool IsInRange(T candidate) { return min.Match( whenClosed: l => max.Match( whenClosed: h => l.CompareTo(candidate) <= 0 && candidate.CompareTo(h) <= 0, whenOpen: h => l.CompareTo(candidate) <= 0 && candidate.CompareTo(h) < 0), whenOpen: l => max.Match( whenClosed: h => l.CompareTo(candidate) < 0 && candidate.CompareTo(h) <= 0, whenOpen: h => l.CompareTo(candidate) < 0 && candidate.CompareTo(h) < 0)); }
This implementation performs a nested Match
to arrive at the appropriate answer. The code isn't as elegant or readable as its F# counterpart, but it comes with comparable compile-time safety. You can't forget a combination, because if you do, your code isn't going to compile.
Still, you can't deny that C# involves more ceremony.
Conclusion #
Once you know how, it's not that difficult to port a functional design from F# or Haskell to a language like C#. The resulting code tends to be more complicated, but to a large degree, it's possible to retain the type safety.
In this article you saw a sketch of how to make that transition, using the Range kata as an example. The resulting C# API is perfectly serviceable, as the test code demonstrates.
Now that we have covered the fundamentals of the Range kata we have learned enough about it to go beyond the exercise and examine some more abstract properties.
Next: Range as a functor.