3
\$\begingroup\$

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?

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.