Routing with the W-Combinator

AKA The Duplication or Warbler Combinator

Apr 9, 2021·

4 min read

Routing with the W-Combinator

The W-Combinator

Combinators are functional patterns that glue together functions. These patterns promote single responsibility and decoupling. The first time I saw the W-Combinator, it looked pretty useless. However, today when I was writing a functional router, I noticed that the W-Combinator could simplify the implementation through re-use.

The W-Combinator is defined as follows:

// lambda definition
(a -> a -> b) -> a -> b

// functional definition
let warbler = f => x => f(x)(x);

Simply put, the W-Combinator takes a two argument function and a single argument and applies that argument twice. You can see why it's also known as the Duplication combinator. In effect, it turns a two argument function into a single argument function.

Functional Routing

Lately, I've been experimenting with using event-based communication for services at the application level. The idea is to be able to create a monolith using the macro architecture at the micro level. This encourages services to be decoupled and can be cleaved off when it becomes big enough to be a microservice. This was heavily inspired by the Java project Vert.x.

If you aren't familiar with this architecture, the TLDR; is that your system has a message queue that services can publish and subscribe to. Each service will react to the appropriate messages. This leads to a highly decoupled system as the services only communicate indirectly through the bus. This decoupling allows for the system to operate when services come up and down, but it does come at a cost - which is beyond the scope of this article.

At the application level, each service is launched using a Start function where the EventBus is injected in. The purpose of the Start function is to initialize the service, but also to subscribe to messages from the event bus. A pattern I've typically used is to create a function that selects the appropriate handler based on the message type. This is effectively a router for callbacks.

Function Router

The function router pattern matches on the type of the message and returns the appropriate handler. This could have also been done with a HashMap as well, but for argument's sake we'll continue with this approach.

internal static Action<IMessage> SelectHandler(IMessage message)
{
  switch (message)
  {
    case DoWorkA _ : return HandleWorkA;
    case DoWorkB _ : return HandleWorkB;
    case DoWorkC _ : return HandleWorkC;
  }

  return _ => { };
}

To allow the service to interact with the EventBus, I setup the listeners in the Start function using:

public void Start(IMessageBus messageBus)
{
  _unsubscribe = messageBus.Subscribe(
    message => SelectHandler(message)(message)
  );
}

Instantly, we can see that SelectHandler is being called with message twice. An interesting question is why not have SelectHandler invoke the Action. The goal was to keep the responsibilities of SelectHandler as narrow as possible. This would avoid SelectHandler from being both the router and invoker. Maybe I'm taking single responsibility too far, but from what I can tell, it's a lot easier to test.

Using the W-Combinator

Hacking the combinator to accept Actions, the definition for the W-Combinator ends up being defined as follows:

// dirty-hacked definition
public static Action<T> Warbler<T>(Func<T, Action<T>> f) => x => f(x)(x);

// strict definition
public static Func<T1, T2> Warbler<T1, T2>(Func<T1, Func<T1, T2>> f) 
  => x => f(x)(x);

The final Start code ends up looking like this:

public void Start(IMessageBus messageBus)
{
  _unsubscribe = messageBus.Subscribe(Warbler(SelectHandler));
}

I can already see you rolling your eyes at this. This is totally unreadable code, but the great thing about combinators are that you can define them for whatever project you need and name them however you want.

If one considers that SelectHandler can be abstracted to a function that accepts a mapping of types to handlers, we can leverage the combinator at the abstraction level without exposing it to consumers. This might be the key thing to consider. It's probably a really bad idea to have combinators showing up everywhere in your code, but if you can leverage them at the abstract layer, they can ensure robustness of code through mathematically proven patterns.

Attributions

Photo by Steve Garvie from Dunfermline, Fife, Scotland, CC BY-SA 2.0, via Wikimedia Commons