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.
Comments
A couple of questions: If you're porting your code from F# to Haskell and back into F#, why not just use Haskell in the first place?
Also, why would you want to mark a function as having side effects in the first place?
Thanks
Kurren, thank you for writing. Why not use Haskell in the first place? In some situations, if that's an option, I'd go for it. In most of my professional work, however, it's not. A bit of background is required, I think. I've spent most of my professional career working with Microsoft technologies. Most of my clients use .NET. The majority of my clients use C#, but some are interested in adopting functional programming. F# is a great bridge to functional programming, because it integrates so well with existing C# code.
Perhaps most importantly is that while these organisations learn F# as a new programming language, and a new paradigm, all other dimensions of software development remain the same. You can use familiar libraries and frameworks, you can use the same Continuous Integration and build tools as you've been used to, you can deploy like you always do, and you can operate and monitor your software like before.
If you start using Haskell, not only will developers have to learn an entire new ecosystem, but if you have a separate operations team, they would have be on board with that as well.
If you can't get the entire organisation to accept Haskell-based software, then F# is a great choice for a .NET shop. Developers will obviously know the difference, but the rest of the organisation can be oblivious to the choice of language.
When it comes to side-effects, that's one of the main motivations behind pure code in the first place. A major source of bugs is that often, programs have unintended side-effects. It's easy to introduce defects in a code base when side-effects are uncontrolled. That's the case for languages like C# and Java, and, unfortunately, F# as well. When you look at a method or function, there's no easy way to determine whether it has side-effects. In other words: all methods or functions could have side effects. (Also: all methods could return null.)
This slows you down when you work on an existing code base, because there's only one way to determine if side-effects or non-deterministic behaviour are part of the code you're about to call: you have to read it all.
On the other hand, if you can tell, just by looking at a function's type, whether or not it has side-effects, you can save yourself a lot of time. By definition, all the pure functions have no side-effect. You don't need to read the code in order to detect that.
In Haskell, this is guaranteed. In F#, you can design your system that way, but some discipline is required in order to uphold such a guarantee.