How to translate a domain-specific sum type into a Church-encoded C# API. An article for object-oriented developers.

This article is part of a series of articles about Church encoding. In the previous articles, you've seen how to implement Boolean logic without Boolean primitives, as well as how to model natural numbers, how to implement a Maybe container, and how to implement an Either container. Common to all four examples is that they're based on sum types with exactly two mutually exclusive cases.

Three binary sum types, and their corresponding match methods.

You may already have noticed that all three translate to Match methods that take two arguments. The translation is so mechanical that you could automate it. Each case in a sum type becomes an argument in a Match method. In this article, you'll see an example of a domain-specific sum type with three cases, translated to Church-encoding.

A payment type model in F# #

In a previous article I described a particular business problem that was elegantly addressed with a discriminated union (sum type) in F#:

type PaymentService = { Name : string; Action : string }
 
type PaymentType =
| Individual of PaymentServiceParent of PaymentServiceChild of originalTransactionKey : string * paymentService : PaymentService

In short, this model enables you to model various payments against a third-party payment service. An individual payment is, as the name implies, a single payment. A parent payment can be used to authorise a series of recurring, automated payments, for example to pay for a subscription. A child payment is one of those recurring payments; it must have a parent payment to authorise it, as automation means that no user interaction takes place.

One task that is easily addressed with the above PaymentType discriminated union is that you can translate the data to JSON in a type-safe manner. The compiler will tell you whether or not you've handled all three cases.

Auxiliary C# classes #

You can Church-encode PaymentType just like Boolean values, natural numbers, Maybe, and Either. Before you do that, however, you need to define the input types involved in each case. These are normal classes, although I prefer to make them immutable:

public class PaymentService
{
    public PaymentService(string name, string action)
    {
        this.Name = name;
        this.Action = action;
    }
 
    public string Name { get; }
 
    public string Action { get; }
}

public class ChildPaymentService
{
    public ChildPaymentService(
        string originalTransactionKey,
        PaymentService paymentService)
    {
        this.OriginalTransactionKey = originalTransactionKey;
        this.PaymentService = paymentService;
    }
 
    public string OriginalTransactionKey { get; }
 
    public PaymentService PaymentService { get; }
}

These are straightforward translations of the F# PaymentService record type, and the tuple associated with the Child case. In a real code base, I'd override Equals for both classes in order to turn them into proper Value Objects, but in order to keep the size of the code down, I omitted doing that here.

Church-encoded payment type #

You can now translate the PaymentType F# discriminated union to a Church-encoded API in C#, starting with the interface:

public interface IPaymentType
{
    T Match<T>(
        Func<PaymentServiceT> individual,
        Func<PaymentServiceT> parent,
        Func<ChildPaymentServiceT> child);
}

Since there's three cases in the sum type, that turns into a Match method with three arguments, each corresponding to one of the cases. As was also the case for the previous articles' INaturalNumber, IMaybe<T>, and IEither<L, R> interfaces, the data associated with each case is modelled as a function from the data to the generic return type T.

Again, following the recipe implied by the previous examples, you should now add a concrete implementation of the IPaymentType interface for each case. It's natural to start with the first argument to the Match method, individual:

public class Individual : IPaymentType
{
    private readonly PaymentService paymentService;
 
    public Individual(PaymentService paymentService)
    {
        this.paymentService = paymentService;
    }
 
    public T Match<T>(
        Func<PaymentServiceT> individual,
        Func<PaymentServiceT> parent,
        Func<ChildPaymentServiceT> child)
    {
        return individual(paymentService);
    }
}

The Individual class adapts a PaymentService value, which it passes as the argument to the individual function argument when Match is called. As you've seen in the previous articles, a particular implementation uses only one of the method arguments, so the two other arguments, parent and child, are simply ignored.

The parent implementation is almost identical:

public class Parent : IPaymentType
{
    private readonly PaymentService paymentService;
 
    public Parent(PaymentService paymentService)
    {
        this.paymentService = paymentService;
    }
 
    public T Match<T>(
        Func<PaymentServiceT> individual,
        Func<PaymentServiceT> parent,
        Func<ChildPaymentServiceT> child)
    {
        return parent(paymentService);
    }
}

The Parent class also adapts a PaymentService value that it passes to a function when Match is called. The only difference is that it calls the parent function instead of the individual function argument.

The third case is handled by the following Child class:

public class Child : IPaymentType
{
    private readonly ChildPaymentService childPaymentService;
 
    public Child(ChildPaymentService childPaymentService)
    {
        this.childPaymentService = childPaymentService;
    }
 
    public T Match<T>(
        Func<PaymentServiceT> individual,
        Func<PaymentServiceT> parent,
        Func<ChildPaymentServiceT> child)
    {
        return child(childPaymentService);
    }
}

While the two other classes both adapt a PaymentService value, a Child object instead composes a ChildPaymentService value. When Match is called, it calls the child function argument with the composed value.

Using the IPaymentType API #

One important feature that I originally had to implement was to translate a payment type value into a JSON document. For the purposes of this example, imagine that you can model the desired JSON document using this Data Transfer Object:

public class PaymentJsonModel
{
    public string Name { getset; }
 
    public string Action { getset; }
 
    public IChurchBoolean StartRecurrent { getset; }
 
    public IMaybe<string> TransactionKey { getset; }
}

This is a mutable object because most .NET serialisation APIs require that the class in question has a parameterless constructor and writeable properties. Notice, however, that in order to demonstrate that all this code still doesn't rely on any primitive Boolean operators and such, the class' properties are defined as IChurchBoolean and IMaybe<string> values, as well as regular string values.

Writing a method that translates any IPaymentType object into a PaymentJsonModel object is now straightforward:

public static PaymentJsonModel ToJson(this IPaymentType payment)
{
    return payment.Match(
        individual : ps =>
            new PaymentJsonModel
            {
                Name = ps.Name,
                Action = ps.Action,
                StartRecurrent = new ChurchFalse(),
                TransactionKey = new Nothing<string>()
            },
        parent : ps =>
            new PaymentJsonModel
            {
                Name = ps.Name,
                Action = ps.Action,
                StartRecurrent = new ChurchTrue(),
                TransactionKey = new Nothing<string>()
            },
        child : cps =>
            new PaymentJsonModel
            {
                Name = cps.PaymentService.Name,
                Action = cps.PaymentService.Action,
                StartRecurrent = new ChurchFalse(),
                TransactionKey = new Just<string>(cps.OriginalTransactionKey)
            });
}

Because the Match method takes three arguments, you have to supply a 'handler' function for each of them, and they all have to have the same return type. In this case they all return a new PaymentJsonModel object, so that requirement is fulfilled. All three lambda expressions simply copy over Name and Action, but they differ in the values they assign to StartRecurrent and TransactionKey.

Tests #

In order to show you that it all works, here's a few examples I wrote as xUnit.net tests:

[Fact]
public void IndividualToJsonReturnsCorrectResult()
{
    var sut = new Individual(new PaymentService("MasterCard""Pay"));
 
    var actual = sut.ToJson();
 
    Assert.Equal("MasterCard", actual.Name);
    Assert.Equal("Pay", actual.Action);
    Assert.False(actual.StartRecurrent.ToBool());
    Assert.True(actual.TransactionKey.IsNothing().ToBool());
}
 
[Fact]
public void ParentToJsonReturnsCorrectResult()
{
    var sut = new Parent(new PaymentService("MasterCard""Pay"));
 
    var actual = sut.ToJson();
 
    Assert.Equal("MasterCard", actual.Name);
    Assert.Equal("Pay", actual.Action);
    Assert.True(actual.StartRecurrent.ToBool());
    Assert.True(actual.TransactionKey.IsNothing().ToBool());
}
 
[Fact]
public void ChildToJsonReturnsCorrectResult()
{
    var sut =
        new Child(
            new ChildPaymentService(
                "12345",
                new PaymentService("MasterCard""Pay")));
 
    var actual = sut.ToJson();
 
    Assert.Equal("MasterCard", actual.Name);
    Assert.Equal("Pay", actual.Action);
    Assert.False(actual.StartRecurrent.ToBool());
    Assert.Equal("12345", actual.TransactionKey.Match("", x => x));
}

All three tests pass.

Summary #

The major advantage of sum types in statically typed languages is that you can make illegal states unrepresentable (a maxim attributed to Yaron Minsky). Specifically, in the business example of payment types shown here, I need to be able to express that only three out of four combinations of start recurrent and original transaction key is legal. Specifically, I needed to express that the combination of start recurrent = true and the presence of a transaction key is illegal. Making such an illegal state unrepresentable is easy with a sum type, but as this article has shown, you can achieve the same goal in C#.

With the API shown here, there's only three possible states (Individual, Child, and Parent). Notice that all three classes hide their data as private class fields, so the only way to extract that data is to call Match. The compiler will make sure that you handle all three cases, because you must supply a function for all three method arguments.

The code shown in this article is available on GitHub.

This article concludes the little series on how to use Church-encoding in C# to create sum types. You may, however, think that it doesn't really feel object-oriented, with its heavy reliance on function arguments (e.g. Func<PaymentService, T>). It turns out, though, that with only a few refactorings, you'll come to the realisation that what you've seen here is isomorphic to a classic design pattern. Read on!

Next: Some design patterns as universal abstractions.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 18 June 2018 12:04:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!