An introduction for object-oriented programmers to the Specification contravariant functor.

This article is an instalment in an article series about contravariant functors. It assumes that you've read the introduction. In the previous article, you saw an example of a contravariant functor based on the Command Handler pattern. This article gives another example.

Domain-Driven Design discusses the benefits of the Specification pattern. In its generic incarnation this pattern gives rise to a contravariant functor.

Interface #

DDD introduces the pattern with a non-generic InvoiceSpecification interface. The book also shows other examples, and it quickly becomes clear that with generics, you can generalise the pattern to this interface:

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T candidate);
}

Given such an interface, you can implement standard reusable Boolean logic such as and, or, and not. (Exercise: consider how implementations of and and or correspond to well-known monoids. Do the implementations look like Composites? Is that a coincidence?)

The ISpecification<T> interface is really just a glorified predicate. These days the Specification pattern may seem somewhat exotic in languages with first-class functions. C#, for example, defines both a specialised Predicate delegate, as well as the more general Func<T, bool> delegate. Since you can pass those around as objects, that's often good enough, and you don't need an ISpecification interface.

Still, for the sake of argument, in this article I'll start with the Specification pattern and demonstrate how that gives rise to a contravariant functor.

Natural number specification #

Consider the AdjustInventoryService class from the previous article. I'll repeat the 'original' Execute method here:

public void Execute(AdjustInventory command)
{
    var productInventory = this.repository.GetByIdOrNull(command.ProductId)
        ?? new ProductInventory(command.ProductId);
 
    int quantityAdjustment = command.Quantity * (command.Decrease ? -1 : 1);
    productInventory = productInventory.AdjustQuantity(quantityAdjustment);
 
    if (productInventory.Quantity < 0)
        throw new InvalidOperationException("Can't decrease below 0.");
 
    this.repository.Save(productInventory);
}

Notice the Guard Clause:

if (productInventory.Quantity < 0)

Image that we'd like to introduce some flexibility here. It's admittedly a silly example, but just come along for the edification. Imagine that we'd like to use an injected ISpecification<ProductInventory> instead:

if (!specification.IsSatisfiedBy(productInventory))

That doesn't sound too difficult, but what if you only have an ISpecification implementation like the following?

public sealed class NaturalNumber : ISpecification<int>
{
    public readonly static ISpecification<int> Specification =
        new NaturalNumber();
 
    private NaturalNumber()
    {
    }
 
    public bool IsSatisfiedBy(int candidate)
    {
        return 0 <= candidate;
    }
}

That's essentially what you need, but alas, it only implements ISpecification<int>, not ISpecification<ProductInventory>. Do you really have to write a new Adapter just to implement the right interface?

No, you don't.

Contravariant functor #

Fortunately, an interface like ISpecification<T> gives rise to a contravariant functor. This will enable you to compose an ISpecification<ProductInventory> object from the NaturalNumber specification.

In order to enable contravariant mapping, you must add a ContraMap method:

public static ISpecification<T1> ContraMap<TT1>(
    this ISpecification<T> source,
    Func<T1, T> selector)
{
    return new ContraSpecification<T, T1>(source, selector);
}
 
private class ContraSpecification<TT1> : ISpecification<T1>
{
    private readonly ISpecification<T> source;
    private readonly Func<T1, T> selector;
 
    public ContraSpecification(ISpecification<T> source, Func<T1, T> selector)
    {
        this.source = source;
        this.selector = selector;
    }
 
    public bool IsSatisfiedBy(T1 candidate)
    {
        return source.IsSatisfiedBy(selector(candidate));
    }
}

Notice that, as explained in the overview article, in order to map from an ISpecification<T> to an ISpecification<T1>, the selector has to go the other way: from T1 to T. How this is possible will become more apparent with an example, which will follow later in the article.

Identity law #

A ContraMap method with the right signature isn't enough to be a contravariant functor. It must also obey the contravariant functor laws. As usual, it's proper computer-science work to actually prove this, but you can write some tests to demonstrate the identity law for the ISpecification<T> interface. In this article, you'll see parametrised tests written with xUnit.net. First, the identity law:

[Theory]
[InlineData(-102)]
[InlineData(  -3)]
[InlineData(  -1)]
[InlineData(   0)]
[InlineData(   1)]
[InlineData(  32)]
[InlineData( 283)]
public void IdentityLaw(int input)
{
    T id<T>(T x) => x;
    ISpecification<intprojection =
        NaturalNumber.Specification.ContraMap<intint>(id);
    Assert.Equal(
        NaturalNumber.Specification.IsSatisfiedBy(input),
        projection.IsSatisfiedBy(input));
}

In order to observe that the two Specifications have identical behaviours, the test has to invoke IsSatisfiedBy on both of them to verify that the return values are the same.

All test cases pass.

Composition law #

Like the above example, you can also write a parametrised test that demonstrates that ContraMap obeys the composition law for contravariant functors:

[Theory]
[InlineData(   "0:05")]
[InlineData(   "1:20")]
[InlineData(   "0:12:10")]
[InlineData(   "1:00:12")]
[InlineData("1.13:14:34")]
public void CompositionLaw(string input)
{
    Func<string, TimeSpan> f = TimeSpan.Parse;
    Func<TimeSpan, intg = ts => (int)ts.TotalMinutes;
 
    Assert.Equal(
        NaturalNumber.Specification.ContraMap((string s) => g(f(s))).IsSatisfiedBy(input),
        NaturalNumber.Specification.ContraMap(g).ContraMap(f).IsSatisfiedBy(input));
}

This test defines two local functions, f and g. Once more, you can't directly compare methods for equality, so instead you have to call IsSatisfiedBy on both compositions to verify that they return the same Boolean value.

They do.

Product inventory specification #

You can now produce the desired ISpecification<ProductInventory> from the NaturalNumber Specification without having to add a new class:

ISpecification<ProductInventory> specification =
    NaturalNumber.Specification.ContraMap((ProductInventory inv) => inv.Quantity);

Granted, it is, once more, a silly example, but the purpose of this article isn't to convince you that this is better (it probably isn't). The purpose of the article is to show an example of a contravariant functor, and how it can be used.

Predicates #

For good measure, any predicate forms a contravariant functor. You don't need the ISpecification interface. Here are ContraMap overloads for Predicate<T> and Func<T, bool>:

public static Predicate<T1> ContraMap<TT1>(this Predicate<T> predicate, Func<T1, T> selector)
{
    return x => predicate(selector(x));
}

public static Func<T1, boolContraMap<TT1>(this Func<T, boolpredicate, Func<T1, T> selector)
{
    return x => predicate(selector(x));
}

Notice that the lambda expressions are identical in both implementations.

Conclusion #

Like Command Handlers and Event Handlers, generic predicates give rise to a contravariant functor. This includes both the Specification pattern, Predicate<T>, and Func<T, bool>.

Are you noticing a pattern?

Next: Reader as a contravariant functor.



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

Thursday, 09 September 2021 09:12:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Thursday, 09 September 2021 09:12:00 UTC