Partial application is dependency injection (by Mark Seemann)

The equivalent of dependency injection in F# is partial function application, but it isn’t functional.

This is the second article in a small article series called from dependency injection to dependency rejection.

People often ask me how to do dependency injection in F#. That’s only natural, since I wrote Dependency Injection in .NET some years ago, and also since I’ve increasingly focused my energy on F# and other functional programming languages.

Over the years, I’ve seen other F# experts respond to that question, and often, the answer is that partial function application is the F# way to do dependency injection. For some years, I believed that as well. It turns out to be true in one sense, but incorrect in another. Partial application is equivalent to dependency injection. It’s just not a functional solution to dealing with dependencies.

(To be as clear as I can be: I’m not claiming that partial application isn’t functional. What I claim is that partial application used for dependency injection isn’t functional.)

Attempted dependency injection using functions

Returning to the example from the previous article, you could try to rewrite MaîtreD.TryAccept as a function:

// 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

You could imagine that this tryAccept function is part of a module called MaîtreD, just to keep the examples as equivalent as possible.

The function takes four arguments. The first is the capacity of the restaurant in question; a primitive integer. The next two arguments, readReservations and createReservation fill the role of the injected IReservationsRepository in the previous article. In the object-oriented example, the TryAccept method used two methods on the repository: ReadReservations and Create. Instead of using an interface, in the F# function, I make the function take two independent functions. They have (almost) the same types as their C# counterparts.

The first three arguments correspond to the injected dependencies in the previous MaîtreD class. The fourth argument is a Reservation value, which corresponds to the input to the previous TryAccept method.

Instead of returning a nullable integer, this F# version returns an int option.

The implementation is also equivalent to the C# example: Read the relevant reservations from the database using the readReservations function argument, and sum over their quantities. Based on the number of already reserved seats, decide whether or not to accept the reservation. If you can accept the reservation, set IsAccepted to true, call the createReservation function argument, and pipe the returned ID (integer) to Some. If you can’t accept the reservation, then return None.

Notice that the first three arguments are ‘dependencies’, whereas the last argument is the ‘actual input’, if you will. This means that you can use partial function application to compose this function.

Application

If you recall the definition of the previous IMaîtreD interface, the TryAccept method was defined like this (C# code snippet):

int? TryAccept(Reservation reservation);

You could attempt to define a similar function with the type Reservation -> int option. Normally, you’d want to do this closer to the boundary of the application, but the following example demonstrates how to ‘inject’ real database operations into the function.

Imagine that you have a DB module with these functions:

module DB =
    // string -> DateTimeOffset -> Reservation list
    let readReservations connectionString date = // ..
    // string -> Reservation -> int
    let createReservation connectionString reservation = // ..

The readReservations function takes a connection string and a date as arguments, and returns a list of reservations for that date. The createReservation function also takes a connection string, as well as a reservation. When invoked, it creates a new record for the reservation and returns the ID of the newly created row. (This sort of API violates CQS, so you should consider alternatives.)

If you partially apply these functions with a valid connection string, both have the type desired for their roles in tryAccept. This means that you can create a function from these elements:

// Reservation -> int option
let tryAcceptComposition =
    let read   = DB.readReservations  connectionString
    let create = DB.createReservation connectionString
    tryAccept 10 read create

Notice how tryAccept itself is partially applied. Only the arguments corresponding to the C# dependencies are passed to it, so the return value is a function that ‘waits’ for the last argument: the reservation. As I’ve attempted to indicate by the code comment above the function, it has the desired type of Reservation -> int option.

Equivalence

Partial application used like this is equivalent to dependency injection. To see how, consider the generated Intermediate Language (IL).

F# is a .NET language, so it compiles to IL. You can decompile that IL to C# to get a sense of what’s going on. If you do that with the above tryAcceptComposition, you get something like this:

internal class tryAcceptComposition@17 : FSharpFunc<Reservation, FSharpOption<int>>
{
    public int capacity;
    public FSharpFunc<Reservation, int> createReservation;
    public FSharpFunc<DateTimeOffset, FSharpList<Reservation>> readReservations;
    internal tryAcceptComposition@17(
        int capacity,
        FSharpFunc<DateTimeOffset, FSharpList<Reservation>> readReservations,
        FSharpFunc<Reservation, int> createReservation)
    {
        this.capacity = capacity;
        this.readReservations = readReservations;
        this.createReservation = createReservation;
    }
    public override FSharpOption<int> Invoke(Reservation reservation)
    {
        return MaîtreD.tryAccept<int>(
            this.capacity, this.readReservations, this.createReservation, reservation);
    }
}

I’ve cleaned it up a bit, mostly by removing all attributes from the various elements. Notice how this is a class, with class fields, and a constructor that takes values for the fields and assigns them. It’s constructor injection!

Partial application is dependency injection.

It compiles, works as expected, but is it functional?

Evaluation

People sometimes ask me: How do I know whether my F# code is functional?

I sometimes wonder about that myself, but unfortunately, as nice a language as F# is, it doesn’t offer much help in that regard. Its emphasis is on functional programming, but it allows mutation, object-oriented programming, and even procedural programming. It’s a friendly and forgiving language. (This also makes it a great ‘beginner’ functional language, because you can learn functional concepts piecemeal.)

Haskell, on the other hand, is a strictly functional language. In Haskell, you can only write your code in the functional way.

Fortunately, F# and Haskell are similar enough that it’s easy to port F# code to Haskell, as long as the F# code already is ‘sufficiently functional’. In order to evaluate if my F# code is properly functional, I sometimes port it to Haskell. If I can get it to compile and run in Haskell, I take that as confirmation that my code is functional.

I’ve previously shown an example similar to this one, but I’ll repeat the experiment here. Will porting tryAccept and tryAcceptComposition to Haskell work?

It’s easy to port tryAccept:

tryAccept :: Int -> (ZonedTime -> [Reservation]) -> (Reservation -> Int) -> Reservation
             -> Maybe Int
tryAccept capacity readReservations createReservation reservation =
  let reservedSeats = sum $ map quantity $ readReservations $ date reservation
  in  if reservedSeats + quantity reservation <= capacity
      then Just $ createReservation $ reservation { isAccepted = True }
      else Nothing

Clearly, there are differences, but I’m sure that you can also see the similarities. The most important feature of this function is that it’s pure. All Haskell functions are pure by default, unless explicitly declared to be impure, and that’s not the case here. This function is pure, and so are both readReservations and createReservation.

The Haskell version of tryAccept compiles, but what about tryAcceptComposition?

Like the F# code, the experiment is to see if it’s possible to ‘inject’ functions that actually operate against a database. Equivalent to the F# example, imagine that you have this DB module:

readReservations :: ConnectionString -> ZonedTime -> IO [Reservation]
readReservations connectionString date = -- ..

createReservation :: ConnectionString -> Reservation -> IO Int
createReservation connectionString reservation = -- ..

Database operations are, by definition, impure, and Haskell admirably models that with the type system. Notice how both functions return IO values.

If you partially apply both functions with a valid connection string, the IO context remains. The type of DB.readReservations connectionString is ZonedTime -> IO [Reservation], and the type of DB.createReservation connectionString is Reservation -> IO Int. You can try to pass them to tryAccept, but the types don’t match:

tryAcceptComposition :: Reservation -> IO (Maybe Int)
tryAcceptComposition reservation =
  let read   = DB.readReservations  connectionString
      create = DB.createReservation connectionString
  in tryAccept 10 read create reservation

This doesn’t compile.

It doesn’t compile, because the database operations are impure, and tryAccept wants pure functions.

In short, partial application used for dependency injection isn’t functional.

Summary

Partial application in F# can be used to achieve a result equivalent to dependency injection. It compiles and works as expected, but it’s not functional. The reason it’s not functional is that (most) dependencies are, by their very nature, impure. They’re either non-deterministic, have side-effects, or both, and that’s often the underlying reason that they are factored into dependencies in the first place.

Pure functions, however, can’t call impure functions. If they could, they would become impure themselves. This rule is enforced by Haskell, but not by F#.

When you inject impure operations into an F# function, that function becomes impure as well. Dependency injection makes everything impure, which explains why it isn’t functional.

Functional programming solves the problem of decoupling (side) effects from program logic another way. That’s the topic of the next article.

Next: Dependency rejection.

This blog is totally free, but if you like it, please buy me a cup of coffee.

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

TOP