Routing with the W-Combinator
AKA The Duplication or Warbler 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