Assertfail

Dependency injection in FSharp

05 Sep 2020

One of the questions around how to write F# is how do we compose bigger business applications. In C# you generally use constructor injection.

Let us start by defining some base functions types and environment that we will use in later code samples:

open FSharpPlus
open FSharpPlus.Data

type IUserRepository =
    abstract GetUser : email : string -> Async<string>

type IShoppingListRepository =
    abstract Add : shoppingList : string list -> string list
type Env() =
    interface IUserRepository with
        member this.GetUser email =
            async { return "Sandeep"}
    interface IShoppingListRepository with
            member this.Add shoppingList =
                shoppingList

One example is to use ReaderT in to be able to provide a bag of dependencies as seen on gitter and here:

let getUser email =
    ReaderT(fun (env : #IUserRepository) -> env.GetUser email )

let addShoppingList shoppingList =
    ReaderT(fun (env : #IShoppingListRepository) -> async { return env.Add shoppingList })

let addShoppingListM email = monad {
    let! user = getUser email
    let shoppingList = ["s"]
    return! addShoppingList shoppingList
}

ReaderT.run (addShoppingListM "a@a")  (Env())
|> fun listA -> async {
    let! list = listA
    printfn "%A" list
} |> Async.Start

There is also a post about using reader monad for dependency injection but without using more advanced techniques such as monad transformers.

Another way to decompose your program is to use currying as seen on f# for fun and profit:

let getUser (repo:IUserRepository) email = repo.GetUser email

let addShoppingList (repo : IShoppingListRepository) shoppingList = async { return repo.Add shoppingList }

let addShoppingListM (getUser: string -> Async<string>) (addShoppingList:string list -> Async<string list>) email = monad {
    let! user = getUser email
    let shoppingList = ["s"]
    return! addShoppingList shoppingList
}

let composeAddShoppingListM ()=
    let env = Env()
    let getUser = getUser (env :> IUserRepository)
    let addShoppingList = addShoppingList (env :> IShoppingListRepository)
    addShoppingListM getUser addShoppingList

let addToShoppingListC = composeAddShoppingListM ()
addToShoppingListC "a@a"
|> fun listA -> async {
    let! list = listA
    printfn "%A" list
    } |> Async.Start

In the above example we reused the same class Env in order to keep the code shorter. You need to have some glue, why it might not be suitable for a solution where you have a lot of functions with a lot of dependencies.

Since F# allows for nice OO programming you can use dependency injection via constructor injection:

type Shopping (userRepository:IUserRepository, shoppingListRepository:IShoppingListRepository)=

    let getUser email = userRepository.GetUser email

    let addShoppingList shoppingList = async { return shoppingListRepository.Add shoppingList }

    member __.AddShoppingListM email = monad {
        let! user = getUser email
        let shoppingList = ["s"]
        return! addShoppingList shoppingList
    }

let env = Env()
let shopping = Shopping (env :> IUserRepository, env :> IShoppingListRepository)

shopping.AddShoppingListM "a@a"
|> fun listA -> async {
    let! list = listA
    printfn "%A" list
} |> Async.Start

I usually write F# with a mix of currying and constructor injection and have not tried out using reader monad for service dependencies. The use of ReaderT lets you remove the amount of parameters you might otherwise need to pass around. One of the neat things about using a monad transformer is that you can then compose it with your base monad that you otherwise use extensively such as async.

Tags


Comments

Comments or errors? Fork this post and do a pull request on github