Functional Dependency Injection Using Monadic Contravariant Functors
There — I knew that title would surprise you! I bet you can feel a rush of excitement, and maybe some trepidation, with beads of perspiration forming on your forehead!
But fret not! You will soon see that functional dependency injection is fun and not that hard, especially using contravariant functors that also happen to be monads.
Quick recap: what is a functor?
As described here, a functor F<A> is basically a wrapper for a type A that offers the convenience of chaining operations using the map() operation. The following NumberContainer class provides a basic example:
The obvious benefit here is that the operations pow, mul3, etc. can be chained together in a highly readable way, by abstracting away the mechanics of storing interim results and passing them on to the next function call.
Now let’s consider functors containing functions
Interesting new possibilities arise if we stretch our imagination a bit further and consider functors that contain objects belonging not to a value type, but to a class of functions with a single input and a single output (we could generalize to a broader class of functions if we wanted, but that’s not the point here). What would be the implications of doing this?
First, what’ll happen to map()?
In general, a function that transforms from type A to type B can be written as f: A -> B.
A “plain” functor F of type A could in general map functions of form A -> B, and the result would be a new functor of type F<B>. In this case, map() would have the form:
map F<A>: (A -> B) -> F<B>
Returning to functors of functions, we can obtain a similar form as follows:
map F<A -> B>: (B -> X) -> F<A -> X>
In other words, based on a wrapped function that transforms an A to a B, and another function that transforms a B to an X, we get a wrapped function that transforms from A to an X. An obvious way to achieve this behavior is to compose the two functions: By running the original function on an input of type A, we obtain a value of type B, which can then be “fed into” the second function, to obtain a value of type X.
Of course, in the case of functors of functions, all of this is a potentiality, as the actual functions won’t be executed until they are explicitly run! The functions are simply composed ‘in memory’, and the newly obtained function is wrapped into the same functor type F. As we will see, this kind of lazy evaluation is a big advantage of functors holding functions: real execution with side effects can be deferred even as the chain of computations is being assembled.
Introducing contramap()
Now that we’ve successfully generalized map() to functors holding functions, let’s see what other operations may emerge based on this generalization.
In our previous derivation of map(), we eventually realized that mapping a function onto a functor holding a function basically results in function composition: if g is mapped onto F<h>, what we get is basically a new functor of type F<gh> — which encapsulates a function gh that operates by first calling h on some input, and then g on the result.
One natural variation, of course, would be to do this the other way around. Who’s to say (other than our type checker, if we have one) that we couldn’t call g first, and then h on its result? Indeed, we could do this if our types aligned properly:
contramap F<A -> B>: (X -> A) -> F<X -> B>
Functors that have a contramap operation are known as contravariant functors. Having a contramap operation is useful if we want to use a functor containing a function, like F<A -> B>, but the input value that we have belongs to type X, and not type A. Let’s consider a short example.
An example with both map() and contramap()
In the following example, we have a type FnWrapper that wraps a function with one argument, and exposes ways both to transform the output — using map() — and to transform the input of that function — using contramap(). (Note that in this example, instead of using a class to define FnWrapper, I used the function-to-object style advocated in the tutorials done by Brian Lonsdorf.)
How nice… As you can see, we wanted to run our function with a string input, but it was actually expecting to receive as input a number. So we used contramap() to make the necessary transformation in advance.
Introducing monadic contravariant functors
Now let’s move on to the icing on the cake: an exciting tool for solving the problem of dependency injection… in an elegant way, of course!
The problem can perhaps best be described based on an example. Consider that as developers creating large-scale applications, we are often confronted with the task of parameterizing the behavior of a system based on so-called environment variables. For example, in a test scenario, we might want to connect to a different database than the one used in production. We might also want to generate log messages that are more verbose than otherwise. This requirement forces us to pass around an environment object in the whole application, even if many parts of the application may have no use for it!
Especially when we want to structure our application in a way that relies on functions composed together with functions of functions of functions, it would be akin to a nightmare if we had to change the signature of all functions and return values so that the composition would work.
Let’s see if we can’t solve this problem by adding a bit of secret sauce to our earlier contravariant functor.
Contravariant monads?
Recall that (by-and-large, without going into formal details!) a monad is just a functional datatype that exposes a flatmap() function. Compared to map(), flatmap() will accept a function that maps not from the wrapped value type to an instance of the same type, but from the wrapped value type to a new monad of the resulting stored value type.
So, for example, if we have a functor F<A>, we could map a function g: A -> B onto it and obtain a functor F<B>. However, if we had a monad of type M<A>, we could flatmap a function g: A ->M<B> onto it to obtain a monad M<B>. The key difference is whether it is the flatmapped function that creates the new monad, or if it is the functor itself, onto which we are mapping our function, that has to wrap the result.
Now let’s consider, in a similar vein to our interpretation of map(), what it would mean to flatmap a function onto our contravariant functor.
Recall that for a functor encapsulating a function g: A -> B, we could write the map operation like so:
map F<A -> B>: (B -> X) -> F<A -> X>
Now, flatmap() is a slight variation on the above:
flatmap Monad<A -> B>: (B -> Monad<C -> X>) -> Monad<C -> X>
(here, I wrote out ‘Monad’ instead of using ‘M’ to make the notation more readable)
Adapting our earlier thought processes to this new case, let’s consider what kind of function the resulting monad might encapsulate. Well, here again, we see that we have a wrapped function transforming a type A into a type B, and another function that transforms something of type B into a Monad that wraps a function transforming a C to an X. Since we are looking to get a Monad of exactly this type (Monad<C -> X>), it seems like we could just compose the two functions, as before.
But wait! to get this monad, we actually need to run the composed functions, not just wrap them inside something (as we did with functors). If we were to choose the latter approach, we would get something of type Monad <A -> Monad <C -> X>>, instead of just Monad<C->X>!
So, in this case, it is not enough to just compose the functions and wrap up the resulting function: the resulting function also has to be executed in order to get to the desired outcome.
If we are especially perceptive, we can also see that this derivation foreshadows the possibility of dependency injection. That’s because the appearance of the new type, C, is something of a mystery: at no point did C figure into the oringal monad, Monad<A -> B>! It is in fact a new type that was introduced into the chain of computations on the spur of the moment.
Based on this, let’s take a look at a final example!
Dependency injection using contravariant monads
To continue our previous example, consider the following modifications:
Here, we can see that flatmap() is implemented exactly as described: a new Monad is created, that contains the executed composition of the original and the flatmapped function. Also, what’s really interesting is that the flatmapped function could make use of both the interim results of the earlier chain of computations (res) as well as the environment variable env which we passed into it only at the very end.
This is the big advantage of monads wrapping functions as opposed to basic types. We can use them to perform a chain of computations, while at the same time introducing external dependencies (using flatmap) that can make use of the result of those computations, but are only selectively referenced. Also, given the powers of contramap, the signatures of the functions called within the chain of computations do not have to be changed to accomodate the external dependencies.
I recommend that you take off a day from work to contemplate the wonders of this pattern. I for one am very grateful to Brian Lonsdorf and others in the FP community for awakening me to its beauty.