The F# web-development framework Giraffe supports a functional style of Dependency Injection using a form of the Reader monad, as explained in this GitBook. I would like to use this pattern more generally, moving away from a service-locator pattern I use today in which my injection sites are coupled to the specific service locator. As such, I have created a small package to facilitate common DI scenarios using the Reader monad pattern. My goal is to be able to write code like this:
let logObject x =
injected {
let! log = inject<ILogger>()
let! serializer = inject<ISerializer>()
return x |> serializer.Serialize |> log.WriteLog
}
First off, here's my implementation of the reader monad:
/// Defines a standard 'Reader' monad
type Reader<'a, 'b> = Reader of ('a -> 'b)
module Reader =
let run x (Reader f) =
f x
let create x =
Reader (fun _ -> x)
let bind f x =
let future state =
let z = run state x
run state (f z)
Reader future
let map f x =
bind (f >> create) x
type ReaderBuilder<'t, 'u> () =
member __.Bind (x, f) = Reader.bind f x
member __.Return (x) = Reader.create x
member __.ReturnFrom x = x
member __.Zero () = Reader.create ()
member __.Delay (f) = f()
member __.Combine (a, b) =
a |> Reader.bind (fun () -> b)
member this.TryFinally(body, compensation) =
try
this.ReturnFrom(body())
finally
compensation()
member this.Using(resource : 'T when 'T :> System.IDisposable, binder : 'T -> Reader<'a, 'b>) : Reader<'a, 'b> =
let body' = fun () -> binder resource
this.TryFinally(body', fun () ->
match resource with
| null -> ()
| disp -> disp.Dispose())
member this.While (guard, body: unit -> Reader<_,_>) =
if not (guard()) then
this.Zero()
else
this.Bind(body(), fun () -> this.While(guard, body))
member this.For (sequence: seq<_>, body) =
this.Using(sequence.GetEnumerator(), fun enum ->
this.While(enum.MoveNext, fun () -> body enum.Current))
[<AutoOpen>]
module ReaderMonad =
let reader<'a, 'b> = ReaderBuilder<'a, 'b>()
Next, I created an Injected
type that wraps up the specific case of Reader
for Reader<IServiceProvider, Result<_,_>>
, which will allow me to use this pattern with any IoC container that implements the System.IServiceProvider
interface:
open System
type DependencyInjectionError =
| NoServiceFound of Type
| UnexpectedDependencyInjectionError of exn
/// Defines a specialized Reader monad for Dependency Injection
type Injected<'t, 'e> = Reader<IServiceProvider, Result<'t, 'e>>
module Injected =
let run x f =
Reader.run x f
let create x =
Reader.create (Ok x)
let bind<'a, 'b, 'e> (f: 'a -> Injected<'b, 'e>) (x: Injected<'a, 'e>) : Injected<'b, 'e> =
let future state =
let result = run state x
match result with
| Ok z ->
run state (f z)
| Error e ->
Error e
Reader future
let bindResult<'a, 'b, 'e> (f: 'a -> Injected<'b, 'e>) (x: Result<'a, 'e>) : Injected<'b, 'e> =
match x with
| Ok z -> f z
| Error e -> Reader (fun _ -> Error e)
let map f x =
bind (f >> create) x
let mapError f (x: Injected<_,_>) : Injected<_,_> =
let (Reader getResult) = x
fun provider ->
let result = getResult provider
match result with
| Ok value -> Ok value
| Error e -> Error (f e)
|> Reader
let ofResult (result: Result<_,_>) : Injected<_,_> =
Reader.create result
let join (elements: Injected<'a,'e> seq) : Injected<'a list, 'e> =
elements |> Seq.fold (fun acc cur ->
fun provider ->
let result = run provider acc
match result with
| Ok values ->
let next = run provider cur
match next with
| Ok value -> Ok (values @ [value])
| Error error -> Error error
| Error error ->
Error error
|> Reader) (create [])
let ignore (i: Injected<_,_>) =
i |> map ignore
type InjectionBuilder<'t> () =
member __.Bind (x, f) : Injected<_,_> = Injected.bind f x
member __.Bind (x, f) : Injected<_,_> = Injected.bindResult f x
member __.Return (x) : Injected<_,_> = Injected.create x
member __.ReturnFrom (x: Injected<_,_>) = x
member __.ReturnFrom (x: Result<_,_>) = Injected.ofResult x
member __.Zero () : Injected<_,_> = Injected.create ()
member __.Delay (f) : Injected<_,_> = f()
member __.Combine (a, b) : Injected<_,_> =
a |> Injected.bind (fun () -> b)
member this.TryFinally(body: unit -> Injected<_,_>, compensation) : Injected<_,_> =
try
this.ReturnFrom(body())
finally
compensation()
member this.Using(resource : 'T when 'T :> System.IDisposable, binder : 'T -> Injected<'a, 'e>) : Injected<'a, 'e> =
let body' = fun () -> binder resource
this.TryFinally(body', fun () ->
match resource with
| null -> ()
| disp -> disp.Dispose())
member this.While (guard, body: unit -> Injected<_,_>) : Injected<_,_> =
if not (guard()) then
this.Zero()
else
this.Bind(body(), fun () -> this.While(guard, body))
member this.For (sequence: seq<_>, body) : Injected<_,_> =
this.Using(sequence.GetEnumerator(), fun enum ->
this.While(enum.MoveNext, fun () -> body enum.Current))
Finally, I created a DependencyInjection
module to facilitate the actual usage of an IServiceProvider
to resolve dependencies and execute the injected
computation using the injected services:
open System
module DependencyInjection =
type IServiceProvider with
member this.GetService<'t>() =
let serviceType = typeof<'t>
try
match this.GetService(serviceType) with
| null -> Error <| NoServiceFound serviceType
| :? 't as service -> Ok service
| _ -> Error <| NoServiceFound serviceType
with ex ->
Error <| UnexpectedDependencyInjectionError ex
let getService<'t> (context : IServiceProvider) =
if typeof<'t>.IsAssignableFrom(typeof<IServiceProvider>)
then context |> unbox<'t> |> Ok
else context.GetService<'t>()
let resolve (container: IServiceProvider) (reader: Injected<_,_>) =
let (Reader f) = reader
f container
[<AutoOpen>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module DependencyInjectionBuilder =
let injected<'t> = InjectionBuilder<'t>()
let inject<'t>() : Injected<'t, DependencyInjectionError> =
Reader (fun (context: IServiceProvider) -> DependencyInjection.getService<'t> context)
I plan to put these modules together into a NuGet package, then use that from my other F# projects to start decoupling my existing code from our specific service locator, replacing direct calls to container.GetService<'t>()
with the new injected
computation and the inject<'t>()
function. This way, I hope to enable better interoperability with the built-in Microsoft.Extensions.DependencyInjection
framework used in .NET Core and be able to leverage whichever DI framework is already wired-up in a given .NET Core application.
Does this look like it would work as I'm intending? Are there any glaring oversights I haven't considered here?