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
.
Do you want to send a comment or give me a hint about any issues with a blog post: Open up an issue on GitHub.
Do you want to fix an error or add a comment published on the blog? You can do a fork of this post and do a pull request on github.
Comments
The most educational source around F# and dependency injection is F# for fun and profit. It’s part of the 2020 advent calendar and is a great source with links to other well written posts.
There is also this post that looks nice Dealing with complex dependency injection in F#