Contravariant Dependency Injection by Mark Seemann
How to use a DI Container in FP... or not.
This article is an instalment in an article series about contravariant functors. It assumes that you've read the introduction, and a few of the examples.
People sometimes ask me how to do Dependency Injection (DI) in Functional Programming (FP). The short answer is that you don't, because dependencies make functions impure. If you're having the discussion on Twitter, you may get some drive-by comments to the effect that DI is just a contravariant functor. (I've seen such interactions take place, but currently can't find the specific examples.)
What might people mean by that?
In this article, I'll explain. You'll also see how this doesn't address the underlying problem that DI makes everything impure. Ultimately, then, it doesn't really matter. It's still not functional.
Partial application as Dependency Injection #
As example code, let's revisit the code from the article Partial application is dependency injection. You may have an impure action like this:
// int -> (DateTimeOffset -> Reservation list) -> (Reservation -> int) -> Reservation // -> int option let tryAccept capacity readReservations createReservation reservation = let reservedSeats = readReservations reservation.Date |> List.sumBy (fun x -> x.Quantity) if reservedSeats + reservation.Quantity <= capacity then createReservation { reservation with IsAccepted = true } |> Some else None
The readReservations
and createReservation
'functions' will in reality be impure actions that read from and write to a database. Since they are impure, the entire tryAccept
action is impure. That's the result from that article.
In this example the 'dependencies' are pushed to the left. This enables you to partially apply the 'function' with the dependencies:
let sut = tryAccept capacity readReservations createReservation
This code example is from a unit test, which is the reason that I've named the partially applied 'function' sut
. It has the type Reservation -> int option
. Keep in mind that while it looks like a regular function, in practice it'll be an impure action.
You can invoke sut
like another other function:
let actual = sut reservation
When you do this, tryAccept
will execute, interact with the partially applied dependencies, and return a result.
Partial application is a standard practice in FP, and in itself it's useful. Using it for Dependency Injection, however, doesn't magically make DI functional. DI remains impure, which also means that the entire sut
composition is impure.
Moving dependencies to the right #
Pushing dependencies to the left enables partial application. Dependencies are typically more long-lived than run-time values like reservation
. Thus, partially applying the dependencies as shown above makes sense. If all dependencies are thread-safe, you can let an action like sut
, above, have Singleton lifetime. In other words, just keep a single, stateless Reservation -> int option
action around for the lifetime of the application, and pass it Reservation
values as they arrive.
Moving dependencies to the right seems, then, counterproductive.
// Reservation -> int -> (DateTimeOffset -> Reservation list) -> (Reservation -> int) // -> int option let tryAccept reservation capacity readReservations createReservation = let reservedSeats = readReservations reservation.Date |> List.sumBy (fun x -> x.Quantity) if reservedSeats + reservation.Quantity <= capacity then createReservation { reservation with IsAccepted = true } |> Some else None
If you move the dependencies to the right, you can no longer partially apply them, so what would be the point of that?
As given above, it seems that you'll have to supply all arguments in one go:
let actual = tryAccept reservation capacity readReservations createReservation
If, on the other hand, you turn all the dependencies into a tuple, some new options arise:
// Reservation -> (int * (DateTimeOffset -> Reservation list) * (Reservation -> int)) // -> int option let tryAccept reservation (capacity, readReservations, createReservation) = let reservedSeats = readReservations reservation.Date |> List.sumBy (fun x -> x.Quantity) if reservedSeats + reservation.Quantity <= capacity then createReservation { reservation with IsAccepted = true } |> Some else None
I decided to count capacity
as a dependency, since this is most likely a value that originates from a configuration setting somewhere. Thus, it'd tend to have a singleton lifetime equivalent to readReservations
and createReservation
.
It still seems that you have to supply all the arguments, though:
let actual = tryAccept reservation (capacity, readReservations, createReservation)
What's the point, then?
Reader #
You can view all functions as values of the Reader profunctor. This, then, is also true for tryAccept
. It's an (impure) function that takes a Reservation
as input, and returns a Reader value as output. The 'Reader environment' is a tuple of dependencies, and the output is int option
.
First, add an explicit Reader
module:
// 'a -> ('a -> 'b) -> 'b let run env rdr = rdr env // ('a -> 'b) -> ('c -> 'd) -> ('b -> 'c) -> 'a -> 'd let dimap f g rdr = fun env -> g (rdr (f env)) // ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b let map g = dimap id g // ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c let contraMap f = dimap f id
The run
function is mostly there to make the Reader concept more explicit. The dimap
implementation is what makes Reader a profunctor. You can use dimap
to implement map
and contraMap
. In the following, we're not going to need map
, but I still included it for good measure.
You can partially apply the last version of tryAccept
with a reservation
:
// sut : (int * (DateTimeOffset -> Reservation list) * (Reservation -> int)) -> int option let sut = tryAccept reservation
You can view the sut
value as a Reader. The 'environment' is the tuple of dependencies, and the output is still int option
. Since we can view a tuple as a type-safe DI Container, we've now introduced a DI Container to the 'function'. We can say that the action is a Reader of the DI Container.
You can run
the action by supplying a container
:
let container = (capacity, readReservations, createReservation) let sut = tryAccept reservation let actual = sut |> Reader.run container
This container
, however, is specialised to fit tryAccept
. What if other actions require other dependencies?
Contravariant mapping #
In a larger system, you're likely to have more than the three dependencies shown above. Other actions require other dependencies, and may not need capacity
, readReservations
, or createReservation
.
To make the point, perhaps the container
looks like this:
let container = (capacity, readReservations, createReservation, someOtherService, yetAnotherService)
If you try to run
a partially applied Reader like sut
with this action, your code doesn't compile. sut
needs a triple of a specific type, but container
is now a pentuple. Should you change tryAccept
so that it takes the bigger pentuple as a parameter?
That would compile, but would be a leaky abstraction. It would also violate the Dependency Inversion Principle and the Interface Segregation Principle because tryAccept
would require callers to supply a bigger argument than is needed.
Instead, you can contraMap
the pentuple:
let container = (capacity, readReservations, createReservation, someOtherService, yetAnotherService) let sut = tryAccept reservation |> Reader.contraMap (fun (c, rr, cr, _, _) -> (c, rr, cr)) let actual = sut |> Reader.run container
This turns sut
into a Reader of the pentuple, instead of a Reader of the triple. You can now run
it with the pentuple container
. tryAccept
remains a Reader of the triple.
Conclusion #
Keep in mind that tryAccept
remains impure. While this may look impressively functional, it really isn't. It doesn't address the original problem that composition of impure actions creates larger impure actions that don't fit in your head.
Why, then, make things more complicated than they have to be?
In FP, I'd look long and hard for a better alternative - one that obeys the functional architecture law. If, for various reasons, I was unable to produce a purely functional design, I'd prefer composing impure actions with partial application. It's simpler.
This article isn't an endorsement of using contravariant DI in FP. It's only meant as an explanation of a phrase like DI is just a contravariant functor.
Next: Profunctors.