A variation of the Composite design pattern uses endomorphic composition. That's still a monoid.

This article is part of a series of articles about design patterns and their category theory counterparts. In a previous article, you learned that the Composite design pattern is simply a monoid.

There is, however, a variation of the Composite design pattern where the return value from one step can be used as the input for the next step.

Endomorphic API #

Imagine that you have to implement some scheduling functionality. For example, you may need to schedule something to happen a month from now, but it should happen on a bank day, during business hours, and you want to know what the resulting date and time will be, expressed in UTC. I've previously covered the various objects for performing such steps. The common behaviour is this interface:

public interface IDateTimeOffsetAdjustment
{
    DateTimeOffset Adjust(DateTimeOffset value);
}

The Adjust method is an endomorphism; that is: the input type is the same as the return type, in this case DateTimeOffset. A previous article already established that that's a monoid.

Composite endomorphism #

If you have various implementations of IDateTimeOffsetAdjustment, you can make a Composite from them, like this:

public class CompositeDateTimeOffsetAdjustment : IDateTimeOffsetAdjustment
{
    private readonly IReadOnlyCollection<IDateTimeOffsetAdjustment> adjustments;
 
    public CompositeDateTimeOffsetAdjustment(
        IReadOnlyCollection<IDateTimeOffsetAdjustment> adjustments)
    {
        if (adjustments == null)
            throw new ArgumentNullException(nameof(adjustments));
 
        this.adjustments = adjustments;
    }
 
    public DateTimeOffset Adjust(DateTimeOffset value)
    {
        var acc = value;
        foreach (var adjustment in this.adjustments)
            acc = adjustment.Adjust(acc);
        return acc;
    }
}

The Adjust method simply starts with the input value and loops over all the composed adjustments. For each adjustment it adjusts acc to produce a new acc value. This goes on until all adjustments have had a chance to adjust the value.

Notice that if adjustments is empty, the Adjust method simply returns the input value. In that degenerate case, the behaviour is similar to the identity function, which is the identity for the endomorphism monoid.

You can now compose the desired behaviour, as this parametrised xUnit.net test demonstrates:

[Theory]
[InlineData("2017-01-31T07:45:55+2""2017-02-28T07:00:00Z")]
[InlineData("2017-02-06T10:03:02+1""2017-03-06T09:03:02Z")]
[InlineData("2017-02-09T04:20:00Z" , "2017-03-09T09:00:00Z")]
[InlineData("2017-02-12T16:02:11Z" , "2017-03-10T16:02:11Z")]
[InlineData("2017-03-14T13:48:29-1""2017-04-13T14:48:29Z")]
public void AdjustReturnsCorrectResult(
    string dtS,
    string expectedS)
{
    var dt = DateTimeOffset.Parse(dtS);
    var sut = new CompositeDateTimeOffsetAdjustment(
        new NextMonthAdjustment(),
        new BusinessHoursAdjustment(),
        new DutchBankDayAdjustment(),
        new UtcAdjustment());
 
    var actual = sut.Adjust(dt);
 
    Assert.Equal(DateTimeOffset.Parse(expectedS), actual);
}

You can see the implementation for all four composed classes in the previous article. NextMonthAdjustment adjusts a date by a month into its future, BusinessHoursAdjustment adjusts a time to business hours, DutchBankDayAdjustment takes bank holidays and weekends into account in order to return a bank day, and UtcAdjustment convert a date and time to UTC.

Monoidal accumulation #

As you've learned in that previous article that I've already referred to, an endomorphism is a monoid. In this particular example, the binary operation in question is called Append. From another article, you know that monoids accumulate:

public static IDateTimeOffsetAdjustment Accumulate(
    IReadOnlyCollection<IDateTimeOffsetAdjustment> adjustments)
{
    IDateTimeOffsetAdjustment acc =
        new IdentityDateTimeOffsetAdjustment();
    foreach (var adjustment in adjustments)
        acc = Append(acc, adjustment);
    return acc;
}

This implementation follows the template previously described:

  • Initialize a variable acc with the identity element. In this case, the identity is a class called IdentityDateTimeOffsetAdjustment.
  • For each adjustment in adjustments, Append the adjustment to acc.
  • Return acc.
This is an entirely automatable process, and it's only C#'s lack of higher-kinded types that prevents us from writing that code once and for all. In Haskell, this general-purpose function exists; it's called mconcat. We'll get back to that in a moment, but first, here's another parametrised unit test that exercises the same test cases as the previous test, only against a composition created by Accumulate:

[Theory]
[InlineData("2017-01-31T07:45:55+2""2017-02-28T07:00:00Z")]
[InlineData("2017-02-06T10:03:02+1""2017-03-06T09:03:02Z")]
[InlineData("2017-02-09T04:20:00Z" , "2017-03-09T09:00:00Z")]
[InlineData("2017-02-12T16:02:11Z" , "2017-03-10T16:02:11Z")]
[InlineData("2017-03-14T13:48:29-1""2017-04-13T14:48:29Z")]
public void AccumulatedAdjustReturnsCorrectResult(
    string dtS,
    string expectedS)
{
    var dt = DateTimeOffset.Parse(dtS);
    var sut = DateTimeOffsetAdjustment.Accumulate(
        new NextMonthAdjustment(),
        new BusinessHoursAdjustment(),
        new DutchBankDayAdjustment(),
        new UtcAdjustment());
 
    var actual = sut.Adjust(dt);
 
    Assert.Equal(DateTimeOffset.Parse(expectedS), actual);
}

While the implementation is different, this monoidal composition has the same behaviour as the above CompositeDateTimeOffsetAdjustment class. This, again, emphasises that Composites are simply monoids.

Endo #

For comparison, this section demonstrates how to implement the above behaviour in Haskell. The code here passes the same test cases as those above. You can skip to the next section if you want to get to the conclusion.

Instead of classes that implement interfaces, in Haskell you can define functions with the type ZonedTime -> ZonedTime. You can compose such functions using the Endo newtype 'wrapper' that turns endomorphisms into monoids:

λ> adjustments = reverse [adjustToNextMonth, adjustToBusinessHours, adjustToDutchBankDay, adjustToUtc]
λ> :type adjustments
adjustments :: [ZonedTime -> ZonedTime]

λ> adjust = appEndo $ mconcat $ Endo <$> adjustments
λ> :type adjust
adjust :: ZonedTime -> ZonedTime

In this example, I'm using GHCi (the Haskell REPL) to show the composition in two steps. The first step creates adjustments, which is a list of functions. In case you're wondering about the use of the reverse function, it turns out that mconcat composes from right to left, which I found counter-intuitive in this case. adjustToNextMonth should execute first, followed by adjustToBusinessHours, and so on. Defining the functions in the more intuitive left-to-right direction and then reversing it makes the code easier to understand, I hope.

(For the Haskell connoisseurs, you can also achieve the same result by composing Endo with the Dual monoid, instead of reversing the list of adjustments.)

The second step composes adjust from adjustments. It first maps adjustments to Endo values. While ZonedTime -> ZonedTime isn't a Monoid instances, Endo ZonedTime is. This means that you can reduce a list of Endo ZonedTime with mconcat. The result is a single Endo ZonedTime value, which you can then unwrap to a function using appEndo.

adjust is a function that you can call:

λ> dt
2017-01-31 07:45:55 +0200

λ> adjust dt
2017-02-28 07:00:00 +0000

In this example, I'd already prepared a ZonedTime value called dt. Calling adjust returns a new ZonedTime adjusted by all four composed functions.

Conclusion #

In general, you implement the Composite design pattern by calling all composed functions with the original input value, collecting the return value of each call. In the final step, you then reduce the collected return values to a single value that you return. This requires the return type to form a monoid, because otherwise, you can't reduce it.

In this article, however, you learned about an alternative implementation of the Composite design pattern. If the method that you compose has the same output type as its input, you can pass the output from one object as the input to the next object. In that case, you can escape the requirement that the return value is a monoid. That's the case with DateTimeOffset and ZonedTime: neither are monoids, because you can't add two dates and times together.

At first glance, then, it seems like a falsification of the original claim that Composites are monoids. As you've learned in this article, however, endomorphisms are monoids, so the claim still stands.

Next: Null Object as identity.



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, 16 April 2018 08:16:00 UTC

Tags



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