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.



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

Monday, 21 March 2022 06:46:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 21 March 2022 06:46:00 UTC