Replace Dependency Injection with a Visitor

In a previous article, you saw how you can model any sum type as a Visitor. Does that mean, then, that you can model a free monad as a Visitor?

Yes, it does.

In the F# free monad recipe, you saw how to refactor any injected, interface-based dependency to a free monad. Since a free monad is nothing but a recursive sum type, this means that you now have the tools at your disposal to refactor your injected dependencies to a Visitor.

To be clear, this is an article exploring the boundaries of what's possible with a language like C#. It's not intended to be an endorsement of a particular way to organise code. A conference talk recording that covers the same example also exists.

Dependency Injection example #

I'll walk you through how to do this. We'll start with an example, which, as usual, is about developing an online restaurant reservations system. The code base you'll see here implements the business logic that decides whether or not to accept a reservation request. All code from this article is available on GitHub.

In order to make the example illustrative, but still manageable, we'll look at a single dependency, defined like this:

public interface IReservationsRepository
{
    // This method doesn't belong on a Repository interface. It ought to be
    // on some sort of ITimeProvider interface, or an extension method
    // thereof, but in order to keep the example refactorings as simple as
    // possible, it'll go here for demo purposes.
    bool IsReservationInFuture(Reservation reservation);
 
    IReadOnlyCollection<Reservation> ReadReservations(DateTimeOffset date);
 
    int Create(Reservation reservation);
}

As the code comment explains, the IsReservationInFuture method doesn't belong on this IReservationsRepository interface. A dedicated 'time provider' dependency would be more appropriate, but as you'll see when you read on, refactoring just one interface to a free monad Visitor is already complicated. Doing it twice would just repeat the process while adding little additional clarity.

Apart from IsReservationInFuture, the IReservationsRepository declares ReadReservations for querying the Repository for existing reservations, and Create for adding a new reservation to the Repository. Notice that the Create method violates the Command Query Separation principle. While better alternatives exist, I decided to design the present example like this because it better illustrates how data could flow through a system.

The only consumer of the interface that we're going to consider is this MaîtreD class:

public class MaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository reservationsRepository)
    {
        Capacity = capacity;
        ReservationsRepository = reservationsRepository;
    }
 
    public int? TryAccept(Reservation reservation)
    {
        if (!ReservationsRepository.IsReservationInFuture(reservation))
            return null;
 
        var reservedSeats = ReservationsRepository
            .ReadReservations(reservation.Date)
            .Sum(r => r.Quantity);
        if (Capacity < reservedSeats + reservation.Quantity)
            return null;
 
        reservation.IsAccepted = true;
        return ReservationsRepository.Create(reservation);
    }
 
    public int Capacity { get; }
 
    public IReservationsRepository ReservationsRepository { get; }
}

This is straightforward example code. First, it queries whether the reservation is in the past, because it makes no sense accepting a reservation in the past.

If it gets past that hurdle, the TryAccept method then queries the injected ReservationsRepository for the reservations already recorded on that date, and calculates the sum of the quantities. This produces reservedSeats - the number of already reserved seats. If the restaurant's Capacity (a Primitive Dependency) is less than the already reserved seats, plus the requested quantity, the method again rejects the reservation. This time, the reason is that there's insufficient remaining capacity to accept it.

Finally, if the reservation makes it past all the guards, IsAccepted is set to true, and the reservation is added to the Repository. Recall that Create returns the ID of the new reservation (presumably a database ID), so that ID is returned from the method. In all other cases, the method returns null. The return type of TryAccept is int? (Nullable<int>), so the int value returned from Create is implicitly converted to int?; the compiler does that.

Testability #

My original motivation for learning about Dependency Injection was that I did Test-Driven Development. Dependency Injection enables you to make automated tests deterministic by replacing common sources of non-determinism with Test Doubles. This is, predictably, also the case here:

[TheoryBookingApiTestConventions]
public void TryAcceptReturnsReservationIdInHappyPathScenario(
    [Frozen]Mock<IReservationsRepository> td,
    Reservation reservation,
    IReadOnlyCollection<Reservation> reservations,
    MaîtreD sut,
    int excessCapacity,
    int expected)
{
    td.Setup(r => r.IsReservationInFuture(reservation)).Returns(true);
    td
        .Setup(r => r.ReadReservations(reservation.Date))
        .Returns(reservations);
    td.Setup(r => r.Create(reservation)).Returns(expected);
    var reservedSeats = reservations.Sum(r => r.Quantity);
    reservation.IsAccepted = false;
    sut = sut.WithCapacity(
        reservedSeats + reservation.Quantity + excessCapacity);
 
    var actual = sut.TryAccept(reservation);
 
    Assert.Equal(expected, actual);
    Assert.True(reservation.IsAccepted);
}

This tests exercises the happy path scenario where the reservation is in the future, and there is enough remaining capacity to accept the request. It uses xUnit.net and Moq with AutoFixture gluing it all together.

Database implementation #

It's nice to be able to test the business logic, but ultimately, you'll need your application to be able to store reservations in some sort of persistent storage like a relational database. That's easy, too. Just implement IReservationsRepository:

public class SqlReservationsRepository : IReservationsRepository
{
    public SqlReservationsRepository(string connectionString)
    {
        this.ConnectionString = connectionString;
    }
 
    public string ConnectionString { get; }
 
    public bool IsReservationInFuture(Reservation reservation)
    {
        return DateTimeOffset.Now < reservation.Date;
    }
 
    public IReadOnlyCollection<Reservation> ReadReservations(
        DateTimeOffset date)
    {
        return this.ReadReservations(
            date.Date,
            date.Date.AddDays(1).AddTicks(-1));
    }
 
    private IReadOnlyCollection<Reservation> ReadReservations(
        DateTimeOffset min,
        DateTimeOffset max)
    {
        var result = new List<Reservation>();
 
        using (var conn = new SqlConnection(this.ConnectionString))
        using (var cmd = new SqlCommand(readByRangeSql, conn))
        {
            cmd.Parameters.Add(new SqlParameter("@MinDate", min));
            cmd.Parameters.Add(new SqlParameter("@MaxDate", max));
 
            conn.Open();
            using (var rdr = cmd.ExecuteReader())
            {
                while (rdr.Read())
                    result.Add(
                        new Reservation
                        {
                            Date = (DateTimeOffset)rdr["Date"],
                            Name = (string)rdr["Name"],
                            Email = (string)rdr["Email"],
                            Quantity = (int)rdr["Quantity"]
                        });
            }
        }
 
        return result;
    }
 
    private const string readByRangeSql = @"
        SELECT [Date], [Name], [Email], [Quantity]
        FROM [dbo].[Reservations]
        WHERE YEAR(@MinDate) <= YEAR([Date])
        AND MONTH(@MinDate) <= MONTH([Date])
        AND DAY(@MinDate) <= DAY([Date])
        AND YEAR([Date]) <= YEAR(@MaxDate)
        AND MONTH([Date]) <= MONTH(@MaxDate)
        AND DAY([Date]) <= DAY(@MaxDate)";
 
    public int Create(Reservation reservation)
    {
        using (var conn = new SqlConnection(ConnectionString))
        using (var cmd = new SqlCommand(createReservationSql, conn))
        {
            cmd.Parameters.Add(
                new SqlParameter("@Date", reservation.Date));
            cmd.Parameters.Add(
                new SqlParameter("@Name", reservation.Name));
            cmd.Parameters.Add(
                new SqlParameter("@Email", reservation.Email));
            cmd.Parameters.Add(
                new SqlParameter("@Quantity", reservation.Quantity));
 
            conn.Open();
            return cmd.ExecuteNonQuery();
        }
    }
 
    private const string createReservationSql = @"
        INSERT INTO [dbo].[Reservations] ([Date], [Name], [Email], [Quantity])
        VALUES (@Date, @Name, @Email, @Quantity)";
}

As has been my position for years, I find it easier to write database implementations using .NET's original ADO.NET API than fighting with an ORM.

Free monad in F# #

In order to refactor IReservationsRepository to a free monad, it's illustrative to first see how it would look in F#. This step is by no means required, but it offers another perspective of the translation that you have to perform. According to the F# free monad recipe, you can represent the IReservationsRepository interface with a sum type that describes the instruction set of the API:

type ReservationsInstruction<'a> =
| IsReservationInFuture of (Reservation * (bool -> 'a))
| ReadReservations of (DateTimeOffset * (Reservation list -> 'a))
| Create of (Reservation * (int -> 'a))

Each case corresponds to a method from the original interface, and each contains a tuple. The first element of the tuple should contain the input to the method, so the first element of IsReservationInFuture is a Reservation value, the first element of ReadReservations is a DateTimeOffset, and the first element of Create is a Reservation, just like IsReservationInFuture.

The second tuple element is a continuation. This is a function an interpreter must call once it's produced the value required to continue. This corresponds to the output value from the interface methods, so the input to the continuation associated with IsReservationInFuture is a Boolean value, and so on.

The rest of the F# code example follows the recipe to the letter, so it's not important to list it here. You're welcome to look in the Git repository, if you'd like to see the details.

Church-encoded instruction set #

From Church encodings we know that we can represent a sum type as an interface with a single Match method. The instruction set is a functor, so it has a generic type argument:

public interface IReservationsInstruction<T>

The Match method must take an argument for each case in the sum type. In the above F# code, you can see that there's three cases, corresponding to the three methods in the original IReservationsRepository interface.

Furthermore, we know from the previous articles on Church encodings that the Match method must be generic, and return a value of its generic type argument:

TResult Match<TResult>(

Third, each argument (i.e. each case from the sum type you're encoding) must have this form:

Func<somethingTResultname,

where something is the type of the data associated with the case, and name is the name of the case.

The names are easy: they're isReservationInFuture, readReservations, and create, but what are the types associated with each case?

The F# free monad recipe gives the answer, which is why I chose to include the above F# code. For instance, the type associated with the IsReservationInFuture case is Reservation * (bool -> 'a). That's a tuple, where the first element is a Reservation 'object', and the second element is a function.

Take it slow for the first case, then. A tuple where the first element is a Reservation has the type:

Tuple<Reservationfunction-type>

where function-type is the type of the continuation function. In F#, that type was bool -> 'a, which means a function that takes a bool as input, and returns a value of the generic type 'a as output. In our Church-encoded C# code, we call that type T, so you need a function from bool to T; that is: Func<bool, T>. If you plug that into the above tuple type, you get:

Tuple<ReservationFunc<boolT>>

Again, you can plug that type into the something place-holder further up, to find the type of the input argument that corresponds to the isReservationInFuture case:

Func<Tuple<ReservationFunc<boolT>>, TResult> isReservationInFuture

Doing this for the two other cases finally reveals the entire Match method:

public interface IReservationsInstruction<T>
{
    TResult Match<TResult>(
        Func<Tuple<ReservationFunc<boolT>>, TResult> isReservationInFuture,
        Func<Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>>, TResult>
            readReservations,
        Func<Tuple<ReservationFunc<intT>>, TResult> create);
}

That's a worthy candidate for the Ugliest C# method signature of 2018 award, I admit, but it's just an intermediate step. This is how the sausage is made.

Implementations of the instruction set #

The IReservationsInstruction<T> interface defines an API. You'll need classes that implement the interface in order to do something useful. As you've seen multiple times in the articles series about Church encodings, you must add an implementation per case. Starting from the top:

public class IsReservationInFuture<T> : IReservationsInstruction<T>
{
    private readonly Tuple<ReservationFunc<boolT>> t;
 
    public IsReservationInFuture(Tuple<ReservationFunc<boolT>> t)
    {
        this.t = t;
    }
 
    public TResult Match<TResult>(
        Func<Tuple<ReservationFunc<boolT>>, TResult> isReservationInFuture,
        Func<Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>>, TResult>
            readReservations,
        Func<Tuple<ReservationFunc<intT>>, TResult> create)
    {
        return isReservationInFuture(this.t);
    }
}

This class simply adapts an 'object' of the type Tuple<Reservation, Func<bool, T>> to the IReservationsInstruction<T> interface. Importantly, it unconditionally calls the Match method's isReservationInFuture argument, while ignoring readReservations and create. This is consistent with the previous incarnations of Church-encodings you've seen. It's an automatable process. You implement the two other cases in the same way:

public class ReadReservations<T> : IReservationsInstruction<T>
{
    private readonly Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>> t;
 
    public ReadReservations(Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>> t)
    {
        this.t = t;
    }
 
    public TResult Match<TResult>(
        Func<Tuple<ReservationFunc<boolT>>, TResult> isReservationInFuture,
        Func<Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>>, TResult>
            readReservations,
        Func<Tuple<ReservationFunc<intT>>, TResult> create)
    {
        return readReservations(this.t);
    }
}

and

public class Create<T> : IReservationsInstruction<T>
{
    private readonly Tuple<ReservationFunc<intT>> t;
 
    public Create(Tuple<ReservationFunc<intT>> t)
    {
        this.t = t;
    }
 
    public TResult Match<TResult>(
        Func<Tuple<ReservationFunc<boolT>>, TResult> isReservationInFuture,
        Func<Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>>, TResult>
            readReservations, 
        Func<Tuple<ReservationFunc<intT>>, TResult> create)
    {
        return create(this.t);
    }
}

If you find the class names odd, then that's a fair criticism. I agree that Create isn't the most object-oriented class name. At this point, though, the design is hardly object-oriented, even though the code in use is C#. You can deal with the names later.

Functor #

The IReservationsInstruction<T> interface is generic, and as expected, it's a functor:

public static IReservationsInstruction<TResult> Select<TTResult>(
    this IReservationsInstruction<T> source,
    Func<TTResult> selector)
{
    return source.Match<IReservationsInstruction<TResult>>(
        isReservationInFuture: t =>
            new IsReservationInFuture<TResult>(
                new Tuple<ReservationFunc<boolTResult>>(
                    t.Item1,
                    b => selector(t.Item2(b)))),
        readReservations: t =>
            new ReadReservations<TResult>(
                new Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, TResult>>(
                    t.Item1,
                    d => selector(t.Item2(d)))),
        create: t =>
            new Create<TResult>(
                new Tuple<ReservationFunc<intTResult>>(
                    t.Item1,
                    r => selector(t.Item2(r)))));
}

Yes, this is horrendous. The F# code is neater:

let private mapI f = function
    | IsReservationInFuture (x, next) -> IsReservationInFuture (x, next >> f)
    | ReadReservations (x, next) -> ReadReservations (x, next >> f)
    | Create (x, next) -> Create (x, next >> f)

Again, this is an intermediary step. Things will get better.

Church-encoded free monad #

Since IReservationsInstruction<T> is a functor, you can package it as a free monad. This entails creating a wrapper 'program' type for it. This is another sum type, in F# written like this:

type ReservationsProgram<'a> =
| Free of ReservationsInstruction<ReservationsProgram<'a>>
| Pure of 'a

The direct translation to Church-encoded C#, then, is to a Match method with two arguments:

public interface IReservationsProgram<T>
{
    TResult Match<TResult>(
        Func<IReservationsInstruction<IReservationsProgram<T>> ,TResult> free,
        Func<TTResult> pure);
}

While the free case looks intimidating, you arrive at it through the same automatic process as already described.

The pure case is implemented by this trivial class:

public class Pure<T> : IReservationsProgram<T>
{
    private readonly T x;
 
    public Pure(T x)
    {
        this.x = x;
    }
 
    public TResult Match<TResult>(
        Func<IReservationsInstruction<IReservationsProgram<T>>, TResult> free,
        Func<TTResult> pure)
    {
        return pure(this.x);
    }
}

The free case is slightly, but not much, more complex:

public class Free<T> : IReservationsProgram<T>
{
    private readonly IReservationsInstruction<IReservationsProgram<T>> i;
 
    public Free(IReservationsInstruction<IReservationsProgram<T>> i)
    {
        this.i = i;
    }
 
    public TResult Match<TResult>(
        Func<IReservationsInstruction<IReservationsProgram<T>>, TResult> free,
        Func<TTResult> pure)
    {
        return free(this.i);
    }
}

Both of them, true to the plan we're following, call their respective method arguments with the objects that they adapt.

Monad #

In C#, the typical monadic bind function is idiomatically called SelectMany, and for various reasons, you need two overloads:

public static IReservationsProgram<TResult> SelectMany<TTResult>(
    this IReservationsProgram<T> source,
    Func<TIReservationsProgram<TResult>> selector)
{
    return source.Match(
        free: i => new Free<TResult>(i.Select(p => p.SelectMany(selector))),
        pure: x => selector(x));
}
 
public static IReservationsProgram<TResult> SelectMany<TUTResult>(
    this IReservationsProgram<T> source,
    Func<TIReservationsProgram<U>> k,
    Func<TUTResult> s)
{
    return source
        .SelectMany(x => k(x)
            .SelectMany(y => new Pure<TResult>(s(x, y))));
}

The bottom of the two overloads is required by C# if you want to be able to support various query syntax language constructs. The top overload utilises Match to dispatch between pure and free. Again, the pure case is easy, because you simply call selector with x, and return its result.

The free case is more complicated. While i.Select is the Select method defined for IReservationsInstruction<T>, p.SelectMany is a recursive call to the method itself.

That's a fly in the ointment, as C# doesn't handle recursion as well as F# or Haskell. It'll work fine as long as you don't blow the stack by producing huge programs that have to be interpreted.

Lifts #

Following the free monad recipe, you'll need to lift each of the instruction set cases to the 'program' type. The following are plain helper methods:

public static IReservationsProgram<bool> IsReservationInFuture(Reservation reservation)
{
    return new Free<bool>(
        new IsReservationInFuture<IReservationsProgram<bool>>(
            new Tuple<ReservationFunc<boolIReservationsProgram<bool>>>(
                reservation,
                x => new Pure<bool>(x))));
}
 
public static IReservationsProgram<IReadOnlyCollection<Reservation>> ReadReservations(
    DateTimeOffset date)
{
    return new Free<IReadOnlyCollection<Reservation>>(
        new ReadReservations<IReservationsProgram<IReadOnlyCollection<Reservation>>>(
            new Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, IReservationsProgram<IReadOnlyCollection<Reservation>>>>(
                date,
                x => new Pure<IReadOnlyCollection<Reservation>>(x))));
}
 
public static IReservationsProgram<int> Create(Reservation reservation)
{
    return new Free<int>(
        new Create<IReservationsProgram<int>>(
            new Tuple<ReservationFunc<intIReservationsProgram<int>>>(
                reservation,
                x => new Pure<int>(x))));
}

Again, the (unfortunately required) type information makes this unreadable, but in fact, not much happens. In F#, the same lift functions are three one-liners:

let isReservationInFuture r = Free (IsReservationInFuture (r, Pure))
 
let readReservations d = Free (ReadReservations (d, Pure))
 
let create r = Free (Create (r, Pure))

These helper methods serve the sole purpose of making it easier to write client code that produces IReservationsProgram<T> 'objects' (they're actually abstract syntax trees).

MaîtreD #

You now have all the building blocks that enable you to refactor MaîtreD.TryAccept to return an IReservationsProgram<int?>:

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity)
    {
        Capacity = capacity;
    }
 
    public IReservationsProgram<int?> TryAccept(Reservation reservation)
    {
        return ReservationsProgram
            .IsReservationInFuture(reservation)
            .SelectMany(isInFuture =>
            {
                if (!isInFuture)
                    return new Pure<int?>(null);
 
                return ReservationsProgram
                    .ReadReservations(reservation.Date)
                    .SelectMany(reservations =>
                    {
                        var reservedSeats = reservations.Sum(r => r.Quantity);
                        if (Capacity < reservedSeats + reservation.Quantity)
                            return new Pure<int?>(null);
 
                        reservation.IsAccepted = true;
                        return ReservationsProgram
                            .Create(reservation)
                            .Select(x => new int?(x));
                    });
            });
    }
 
    public int Capacity { get; }
}

This is hardly as pretty as the original version that used Dependency Injection, but you should notice an interesting effect: by returning a free monad, you get rid of the injected dependency. While the code still depends on Capacity, that's just a read-only number.

Through the so-called query syntax, C# offers some syntactic sugar for monadic composition, similar to F#'s computation expressions or Haskell's do notation. Where its utility ends, though, is exactly where you need it. Unfortunately, it doesn't support branching (the use of if statements and such) inside of from x in xs expressions, so while we've supplied the requisite SelectMany methods for IReservationsProgram<T>, you can't use them.

Instead, you have to resort to branching inside of each continuation, which, unfortunately, pulls you towards arrow code.

The TryAccept method starts by calling the IsReservationInFuture helper method (defined above), which returns an IReservationsProgram<bool> object. You can't easily pull the bool value out of the container, but you can use SelectMany to define what happens next.

If the reservations is in the future, an interpreter should call the continuation with true, and otherwise, with false. Thus, you can branch on that Boolean value for the next step. If the reservations turned out to be in the past, the return value ought to be null, but you have to package that in an IReservationsProgram<int?>. The correct way to do that is to wrap it in a Pure object.

If the reservation, on the other hand, turned out to be in the future, the program can continue. It proceeds to call the ReadReservations helper method, and again uses SelectMany to define what happens next. An interpreter will supply reservations (presumably read from an actual database), and the code inside the continuation decides what to do next. In the case of insufficient remaining capacity, you return another null wrapped in Pure, but if you decide to accept the reservation, you can finally call the Create helper method and return its return value. Here, however, C#'s implicit conversion no longer works, so you have to explicitly use Select to turn the int into an int?.

Interpreters #

You can still unit test the code by supplying a test-specific interpreter, but in the interest of keeping this article to essentials, I'll refer you to the code repository if you want to see how the tests look at this point.

You're probably more interested in seeing how an interpreter actually interacts with a real database, like the above SqlReservationsRepository class did. The scenario is still the same, only the specifics have changed. Instead of implementing an interface, you'll now have to interpret an IReservationsProgram<int?>. How do you do that?

You do it like you'd traverse any other Church-encoded sum type: use the Match method and supply a handler for each of the cases:

public static T Interpret<T>(
    this IReservationsProgram<T> program,
    string connectionString)
{
    return program.Match(
        pure: x => x,
        free: i => i.Match(
            isReservationInFuture: t =>
                t.Item2(IsReservationInFuture(t.Item1))
                    .Interpret(connectionString),
            readReservations: t =>
                t.Item2(ReadReservations(t.Item1, connectionString))
                    .Interpret(connectionString),
            create: t =>
                t.Item2(Create(t.Item1, connectionString))
                    .Interpret(connectionString)));
}

Since a free monad is a nested, recursive sum type, you'll first have to supply handlers for the pure and free cases. The pure case is, as always, trivial. This is where you finally encounter a leaf node in the abstract syntax tree you're traversing. This is where you get to return the final value of the program.

In the free case, on the other hand, you'll need to handle an IReservationsInstruction<IReservationsProgram<T>> value, which is another Church-encoded sum type. It looks daunting, but is an entirely automatic refactoring: just call Match on that object as well, and supply a handler for each of the cases.

Recall that each of the cases of IReservationsInstruction<T> contains a tuple. The first element (t.Item1) is a value to be used as an input argument for the interpreter. The second element (t.Item2) is a function. Notice that in all three cases, this interpreter calls a helper method with t.Item1 and then calls the continuation t.Item2 with the return value from the helper method; e.g. t.Item2(IsReservationInFuture(t.Item1)). All three continuation functions return a new IReservationsProgram<T>, representing the next step in the program, so you'll have to recursively call Interpret again.

The IsReservationInFuture helper method is simple:

public static bool IsReservationInFuture(Reservation reservation)
{
    return DateTimeOffset.Now < reservation.Date;
}

Notice that this is an entirely normal static helper method that every C# programmer should know how to write. You're now back on familiar territory. The same goes for the two other helper methods ReadReservations and Create. They're slightly refactored versions of the above SqlReservationsRepository methods, so I'm not going to repeat them here. Again, you're welcome to look at the details in the code repository.

Refactor instruction arguments to Parameter Object #

As you know from another article, you can refactor any Church-encoded sum type to a Visitor. If you want to do it step-wise, you start by introducing a Parameter Object, as suggested by Refactoring. There's two sum types in play in this code base, but you can arbitrarily choose to start with the instruction set. Instead of taking three method arguments, change the Match method so that it takes only a single argument:

public interface IReservationsInstruction<T>
{
    TResult Match<TResult>(
        ReservationsInstructionParameters<TTResult> parameters);
}

The new Parameter Object looks like this:

public class ReservationsInstructionParameters<TTResult>
{
    public ReservationsInstructionParameters(
        Func<Tuple<ReservationFunc<boolT>>, TResult> isReservationInFuture,
        Func<Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>>, TResult>
            readReservations,
        Func<Tuple<ReservationFunc<intT>>, TResult> create)
    {
        this.IsReservationInFuture = isReservationInFuture;
        this.ReadReservations = readReservations;
        this.Create = create;
    }
 
    public Func<Tuple<ReservationFunc<boolT>>, TResult> IsReservationInFuture { get; }
    public Func<Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>>, TResult>
        ReadReservations { get; }
    public Func<Tuple<ReservationFunc<intT>>, TResult> Create { get; }
}

This change clearly just moves things around, so nothing much is yet gained. You'll need to adjust other parts of the code in order to pass an instance of this Parameter Object to the Match method, but that's trivial and automatable work, so I'll skip showing it.

If you've noticed that the ReservationsInstructionParameters<T, TResult> Parameter Object is nothing but a container of three functions, you may not be impressed, but from Object isomorphisms we know that a tuple (or record) of functions is isomorphic to an object, thereby nicely putting everything in place for the next change.

Refactor instruction set Parameter Object to an interface #

Instead of a record of three functions, refactor the Parameter Object to an interface with three members:

public interface IReservationsInstructionParameters<TTResult>
{
    TResult IsReservationInFuture(Tuple<ReservationFunc<boolT>> t);
    TResult ReadReservations(Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>> t);
    TResult Create(Tuple<ReservationFunc<intT>> t);
}

Each function is now a method on the interface. Still not perfect, but better. Again, you have to make all sort of adjustments to code that interacts with the Match method. This is the most laborious refactoring, because in every place where before you could simply pass lambda expressions, you now have to introduce explicit classes that implement the interface.

For example, the database interpreter now has to look like this:

public static T Interpret<T>(
    this IReservationsProgram<T> program,
    string connectionString)
{
    return program.Match(
        pure: x => x,
        free: i => i.Match(
            new InterpretReservationsInstructionParameters<T>(
                connectionString)));
}

So far, we've only started refactoring the instruction set, so you still need to handle IReservationsProgram<T> values by supplying two lambda expressions to its Match method. When handling the instruction i, however, you must now supply an implementation of the new IReservationsInstructionParameters<T, TResult> interface.

You can do that by creating a new private, nested class:

private class InterpretReservationsInstructionParameters<T> :
    IReservationsInstructionParameters<IReservationsProgram<T>, T>

As an example, this class implements the ReadReservations method like this:

public T ReadReservations(
    Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, IReservationsProgram<T>>> t)
{
    var reservations = ReadReservations(
        t.Item1.Date,
        t.Item1.Date.AddDays(1).AddTicks(-1));
    return t.Item2(reservations).Interpret(connectionString);
}
 
private IReadOnlyCollection<Reservation> ReadReservations(
    DateTimeOffset min,
    DateTimeOffset max)

The method first calls a helper method also called ReadReservations to read the reservations from the database. That helper method is a perfectly normal C# method that queries a database. Its implementation is equivalent to the above SqlReservationsRepository.ReadReservations implementation, so while I've included its signature in the above code listing, there's no need to repeat the method body here.

Keep in mind that t is the horribly typed tuple declared as the method argument, so t.Item1 is just a DateTimeOffset value that you can pass as argument(s) to the ReadReservations helper method.

The helper method returns reservations, which is an IReadOnlyCollection<Reservation>. You can now (as before) call the continuation function t.Item2 with that object. The continuation returns a new IReservationsProgram<T> that you'll then have to recursively handle by calling Interpret again. This is just like before the refactoring.

Rename instruction set API #

You're simply following the refactoring steps outlined in Visitor as a sum type, so now you can rename the types involved with the instruction set to follow the naming conventions of the Visitor design pattern:

public interface IReservationsInstructionVisitor<TTResult>
{
    TResult VisitIsReservationInFuture(Tuple<ReservationFunc<boolT>> t);
    TResult VisitReadReservations(Tuple<DateTimeOffsetFunc<IReadOnlyCollection<Reservation>, T>> t);
    TResult VisitCreate(Tuple<ReservationFunc<intT>> t);
}

This is the interface previously named IReservationsInstructionParameters<T, TResult> renamed, and with the word Visit affixed. Likewise, you can also make similar changes to the IReservationsInstruction<T> interface:

public interface IReservationsInstruction<T>
{
    TResult Accept<TResult>(IReservationsInstructionVisitor<TTResult> visitor);
}

These changes are simple rename refactorings, so while they affect much other code, I'm not going to list it all.

Make program API a Visitor #

Following the same refactoring steps, you can also turn IReservationsProgram<T> into an application of the Visitor design pattern:

public interface IReservationsProgram<T>
{
    TResult Accept<TResult>(IReservationsProgramVisitor<TTResult> visitor);
}

You simply rename the Match method to Accept, and call the object passed to it visitor. The Visitor itself is defined like this:

public interface IReservationsProgramVisitor<TTResult>
{
    TResult VisitFree(IReservationsInstruction<IReservationsProgram<T>> i);
    TResult VisitPure(T x);
}

While these steps conclude the refactoring steps outlined in the previously mentioned article on how to turn a Church-encoded sum type into a Visitor, the resulting code can still benefit from further clean-up.

Refactor tuple argument to argument list #

If you consider the current definition of IReservationsInstructionVisitor<T, TResult> (see above), you'll notice that each method takes a single argument in the shape of a tuple. From Argument list isomorphisms you know that you can refactor such a method into a method that takes a normal argument list:

public interface IReservationsInstructionVisitor<TTResult>
{
    TResult VisitIsReservationInFuture(
        Reservation reservation,
        Func<boolT> continuation);
    TResult VisitReadReservations(
        DateTimeOffset date,
        Func<IReadOnlyCollection<Reservation>, T> continuation);
    TResult VisitCreate(
        Reservation reservation,
        Func<intT> continuation);
}

Each of these methods now take an input argument (e.g. a Reservation or a DateTimeOffset) and a continuation function. This already improves the API.

SQL Visitor #

In an attempt to make things look proper object-oriented, then, the journey is complete. Instead of a SqlReservationsRepository, you instead need a SqlReservationsProgramVisitor<T>:

public class SqlReservationsProgramVisitor<T> :
    IReservationsProgramVisitor<TT>,
    IReservationsInstructionVisitor<IReservationsProgram<T>, T>
{
    private readonly string connectionString;
 
    public SqlReservationsProgramVisitor(string connectionString)
    {
        this.connectionString = connectionString;
    }
 
    public T VisitPure(T x)
    {
        return x;
    }
 
    public T VisitFree(IReservationsInstruction<IReservationsProgram<T>> i)
    {
        return i.Accept(this);
    }
 
    public T VisitIsReservationInFuture(
        Reservation reservation,
        Func<boolIReservationsProgram<T>> continuation)
    {
        var isInFuture = DateTimeOffset.Now < reservation.Date;
        return continuation(isInFuture).Accept(this);
    }
 
    public T VisitReadReservations(
        DateTimeOffset date,
        Func<IReadOnlyCollection<Reservation>, IReservationsProgram<T>> continuation)
    {
        var reservations = ReadReservations(
            date.Date,
            date.Date.AddDays(1).AddTicks(-1));
        return continuation(reservations).Accept(this);
    }
 
    private IReadOnlyCollection<Reservation> ReadReservations(
        DateTimeOffset min,
        DateTimeOffset max)
    {
        var result = new List<Reservation>();
 
        using (var conn = new SqlConnection(connectionString))
        using (var cmd = new SqlCommand(readByRangeSql, conn))
        {
            cmd.Parameters.Add(new SqlParameter("@MinDate", min));
            cmd.Parameters.Add(new SqlParameter("@MaxDate", max));
 
            conn.Open();
            using (var rdr = cmd.ExecuteReader())
            {
                while (rdr.Read())
                    result.Add(
                        new Reservation
                        {
                            Date = (DateTimeOffset)rdr["Date"],
                            Name = (string)rdr["Name"],
                            Email = (string)rdr["Email"],
                            Quantity = (int)rdr["Quantity"]
                        });
            }
        }
 
        return result;
    }
 
    private const string readByRangeSql = @"
        SELECT [Date], [Name], [Email], [Quantity]
        FROM [dbo].[Reservations]
        WHERE YEAR(@MinDate) <= YEAR([Date])
        AND MONTH(@MinDate) <= MONTH([Date])
        AND DAY(@MinDate) <= DAY([Date])
        AND YEAR([Date]) <= YEAR(@MaxDate)
        AND MONTH([Date]) <= MONTH(@MaxDate)
        AND DAY([Date]) <= DAY(@MaxDate)";
 
    public T VisitCreate(
        Reservation reservation,
        Func<intIReservationsProgram<T>> continuation)
    {
        return continuation(Create(reservation)).Accept(this);
    }
 
    private int Create(Reservation reservation)
    {
        using (var conn = new SqlConnection(connectionString))
        using (var cmd = new SqlCommand(createReservationSql, conn))
        {
            cmd.Parameters.Add(
                new SqlParameter("@Date", reservation.Date));
            cmd.Parameters.Add(
                new SqlParameter("@Name", reservation.Name));
            cmd.Parameters.Add(
                new SqlParameter("@Email", reservation.Email));
            cmd.Parameters.Add(
                new SqlParameter("@Quantity", reservation.Quantity));
 
            conn.Open();
            return cmd.ExecuteNonQuery();
        }
    }
 
    private const string createReservationSql = @"
        INSERT INTO [dbo].[Reservations] ([Date], [Name], [Email], [Quantity])
        VALUES (@Date, @Name, @Email, @Quantity)";
}

Most of this code is similar to the SqlReservationsRepository from the beginning of the article. The IReservationsRepository interface no longer exists; instead this Visitor implements two interfaces: IReservationsProgramVisitor<T, T> and IReservationsInstructionVisitor<IReservationsProgram<T>, T>. A free monad recursively interacts with itself by going back and forth between an 'instruction' and the overall 'program'. Implementing both interfaces in a single class makes it much easier to transition between these two views, as you no longer have to pass e.g. the connectionString around between various objects.

This also means that instead of having to rely on an extension method in order to be able to continue, notice that each method can now continue by calling Accept(this).

Instead of injecting IReservationsRepository into objects, you can call methods that return IReservationsProgram<T> objects and then interpret them using the above SqlReservationsProgramVisitor<T>. For example, you could call TryAccept with a Reservation object:

var p = maîtreD.TryAccept(reservation);

The p variable is an IReservationsProgram<int?> object, where the int? represents a potential reservation ID. At this point, p is a 'program', and you can 'run' it by asking it to accept a Visitor:

var id = p.Accept(new SqlReservationsProgramVisitor<int?>(connectionString));

When Accept returns, id (an int?) has a value, or is null, according to the exact details of reservation and the state of the database.

Testability #

Is this design still testable? Yes, indeed, the overall free monad design is pure, and thereby inherently testable. Instead of using a dynamic mock library like Moq, you can add a test-specific implementation of the free monad Visitor to your unit testing code base:

public class StubReservationsVisitor<T> :
    IReservationsProgramVisitor<TT>,
    IReservationsInstructionVisitor<IReservationsProgram<T>, T>
{
    private readonly bool isInFuture;
    private readonly IReadOnlyCollection<Reservation> reservations;
    private readonly int id;
 
    public StubReservationsVisitor(
        bool isInFuture,
        IReadOnlyCollection<Reservation> reservations,
        int id)
    {
        this.isInFuture = isInFuture;
        this.reservations = reservations;
        this.id = id;
    }
 
    public T VisitPure(T x)
    {
        return x;
    }
 
    public T VisitFree(IReservationsInstruction<IReservationsProgram<T>> i)
    {
        return i.Accept(this);
    }
 
    public T VisitIsReservationInFuture(
        Reservation reservation,
        Func<boolIReservationsProgram<T>> continuation)
    {
        return continuation(isInFuture).Accept(this);
    }
 
    public T VisitReadReservations(
        DateTimeOffset date,
        Func<IReadOnlyCollection<Reservation>, IReservationsProgram<T>> continuation)
    {
        return continuation(reservations).Accept(this);
    }
 
    public T VisitCreate(
        Reservation reservation,
        Func<intIReservationsProgram<T>> continuation)
    {
        return continuation(id).Accept(this);
    }
}

As a true Stub, this implementation ignores the input values and returns pre-configured values. When VisitIsReservationInFuture is called, for example, the implementation ignores the reservation argument and instead 'returns' the isInFuture class field by calling continuation(isInFuture).

You can exercise the happy-path test case from the beginning of this article as a unit test using this Stub Visitor:

[TheoryBookingApiTestConventions]
public void TryAcceptReturnsReservationIdInHappyPathScenario(
    Reservation reservation,
    IReadOnlyCollection<Reservation> reservations,
    MaîtreD sut,
    int excessCapacity,
    int expected)
{
    var reservedSeats = reservations.Sum(r => r.Quantity);
    reservation.IsAccepted = false;
    sut = sut.WithCapacity(
        reservedSeats + reservation.Quantity + excessCapacity);
 
    var actual = sut.TryAccept(reservation);
 
    Assert.Equal(
        expected,
        actual.Accept(new StubReservationsVisitor<int?>(true, reservations, expected)));
    Assert.True(reservation.IsAccepted);
}

This test still uses AutoFixture to create objects such as reservation, reservations, and so on, but instead of relying on dynamic mocks injected into the sut, it uses the StubReservationsVisitor<T> class to interpret the return value from TryAccept.

Arrow code #

From an external perspective, as a user of MaîtreD.TryAccept, the API looks exotic, but to a degree, fairly object-oriented, with its implementation of the Visitor design pattern. Looking at the above internal implementation of the method, however, reveals the most problematic part of this entire exercise. The code doesn't look nice.

Not only do we have nested closures, but it also looks like the dreaded Arrow Code anti-pattern. Partially, the problem is that while C# has some support for monadic syntactic sugar, in the form of query expressions, you can't branch inside a query expression. That's not the whole problem, though. Even in F#, with a computation expression, the equivalent code would look like this:

let tryAccept capacity reservation = reservations {
    let! isInFuture = isReservationInFuture reservation
 
    if not isInFuture
    then return None
    else    
        let! reservations = readReservations reservation.Date
        let reservedSeats = List.sumBy (fun r -> r.Quantity) reservations
 
        if (capacity < reservedSeats + reservation.Quantity)
        then return None
        else
            let! reservationId = create { reservation with IsAccepted = true }
            return Some reservationId }

While this looks smoother, the code still exhibits the arrow shape. There are ways to address that problem, but that's a topic for the next article.

Conclusion #

Even in C#, you can refactor from Dependency Injection to a free monad, implemented as a Visitor. Should you, though?

As a rule of thumb, no, I don't think it's a good idea. Free monads are worth considering in Haskell where they are much closer to being 'free', in the sense that there's little programming overhead involved with defining and using them. Already when you translate a free monad to F# do you discover how much boilerplate programming is involved. Still, due to F#'s computation expressions, a free monad may occasionally be worth considering. Both in implementation and at call sites, you can make the API easy to use.

In C#, you run into its lack of support for branching inside monadic composition. Not only does a free monad look non-idiomatic in C#, but writing the 'programs' without good syntactic sugar for monads make this alternative even less compelling. To make matters worse, the boilerplate code you have to write in C# is positively ugly. As this article is a result of an experiment, I admit that I have no production experience with free monads as Visitors in C#. Still, due to the lack of type inference in crucial parts, I'd venture the guess that the implementation would also prove to be frustratingly refactor-resistant.

Perhaps, one day, you'll run into an edge case where you have a dependency that could benefit from being refactored to a Visitor, but in general, I'd presume that Dependency Injection in C# is the lesser of two evils. Still, I think it's interesting, and illustrative, that it's possible to refactor an injected dependency to a free monad - even in C#!

Next: Flattening arrow code using a stack of monads.



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

Tuesday, 24 July 2018 07:26:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Tuesday, 24 July 2018 07:26:00 UTC