(TL;DR: Intro to the reader-monad in F# for dependency-injection – see the source)
intro: the problem
Let’s say you have a function f
that needs an object implementing some interface
type InterfaceA = abstract MethodA : string -> string
to do it’s work.
a dirty solution (not advised)
Another solution that can work is to make the dependency a mutable part of your defining module:
module MyModule = let DependencyA = ref DefaultImplementationA let f x = ... use (!DependencyA).MethodA ...
I would not advise using this for obvious reasons (hard to test if tests run parallel, etc.) but it can come handy from time to time.
For example I sometimes like to use these for static-parameters/feature-switches and cross-cutting-concerns like logging – but really: use with care.
the usual solution
Using object oriented techniques you would most likely use constructor-injection to solve this:
type ConstructorInjection (i : InterfaceA) = member __.f x = ... do something with i.MethodA ...
But isn’t that overkill? We make a new class only to inject the interface. What’s even worse: we now need an instances of ConstructorInjection
to even get the f
back.
just make it explicit
In most cases I just make the dependencies into explicit arguments and deal use using partial application:
let f (i : InterfaceA) (x : X) : Y = ...
let f` x = f myImplementation x
many dependencies
So what if you have to deal with many dependencies? That’s usually when you start looking for an DI-container to keep track of all the stuff. Keeping it simple you can just collect all dependencies you have in a dependencies structure – for example a record like this:
type Dependencies = { interfaceA : InterfaceA ; interfaceB : InterfaceB // ... }
But of course you probably would not want this to leak into your modules everywhere (meaning: don’t use Dependencies
as parameters to your functions if they really only need InterfaceA
). Instead let the caller handle the projections:
let deps : Dependencies = ... let f (i : InterfaceA) x = ... ... let res = f deps.interfaceA argX
the fancy solution
You might have guessed it already: there is a fancy solution and yeah it’s involving the M-word.
Functions depending on InterfaceA
will look something like this: InterfaceA -> additional args -> result
. Let’s change this a bit: First flip the arguments into additional args -> InterfaceA -> result
which is really additional args -> (InterfaceA -> result)
. Then extract the common part into a type:
type UsingA<'result> = InterfaceA -> 'result
Now the types work out into additional args -> UsingA<'result>
and if you have no additional arguments it will just be an value of UsingA
.
Abstracting it further by making the dependency generic also we end up with:
type ReaderM<'dependency,'result> = 'dependency -> 'result let run (d : 'dependency) (r : ReaderM<'dependency,'result>) : ' result = r d
and our function now has a new type f : X -> ReaderM<InterfaceA,Y>
implementing the usual patterns
It turns out, that ReaderM
falls into several patterns, so let’s start collecting some:
constant readers
It’s useful to have a ReaderM
value that does always returns a constant:
let constant (c : 'c) : ReaderM<_,'c> = fun _ -> c
functor
ReaderM
is a functor – let’s give it a map
:
let map (f : 'a -> 'b) (r : ReaderM<'d, 'a>) : ReaderM<'d,'b> = r >> f let (<?>) = map
applicative-functor
let apply (f : ReaderM<'d, 'a->'b>) (r : ReaderM<'d, 'a>) : ReaderM<'d, 'b> = fun dep -> let f' = run dep f let a = run dep r f' a let (<*>) = apply
monad
Of course ReaderM
is a monad 😉
let bind (m : ReaderM<'d, 'a>) (f : 'a -> ReaderM<'d,'b>) : ReaderM<'d, 'b> = fun dep -> f (run dep m) |> run dep let (>>=) m f = bind m f type ReaderMBuilder internal () = member __.Bind(m, f) = bind m f member __.Return(v) = constant v member __.ReturnFrom(v) = v member __.Delay(f) = f () let Do = ReaderMBuilder()
lifting it
Now let’s make it easier to create ReaderM<_,_>
valued computations from functions with dependencies (like our f
) above by lifting those into the monad:
let lift1 (f : 'd -> 'a -> 'out) : 'a -> ReaderM<'d, 'out> = fun a dep -> f dep a let lift2 (f : 'd -> 'a -> 'b -> 'out) : 'a -> 'b -> ReaderM<'d, 'out> = fun a b dep -> f dep a b let lift3 (f : 'd -> 'a -> 'b -> 'c -> 'out) : 'a -> 'b -> 'c -> ReaderM<'d, 'out> = fun a b c dep -> f dep a b c // ...
And there is another way you can lift stuff: Think back on the situation above where you have a function depending on InterfaceA
but you wrapped an instance into a Dependencies
structure. Here you can lift a ReaderM<InterfaceA,_>
computation into a ReaderM<Dependencies,_>
computation when you have an projection from Dependencies -> InterfaceA
:
let liftDep (proj : 'd2 -> 'd1) (r : ReaderM<'d1, 'out>) : ReaderM<'d2, 'out> = proj >> r
Example
Let’s put this all into a short example:
Imagine you have these two interfaces (and some implementations):
type IResources = abstract GetString : unit -> string let resource = { new IResources with member __.GetString () = "World" } type IOutput = abstract Print : string -> unit let output = { new IOutput with member __.Print s = printfn "%s" s }
We collect them into a Dependencies
type – to keep things simple it’s just a tuple of those two:
type Dependencies= IResources * IOutput let config = (resource, output)
Now we need some functions working with these dependencies:
let getWord = lift1 (fun (res : IResources) -> res.GetString) () let print = lift1 (fun (out : IOutput) -> out.Print)
And our task is to get a word from the resources using the GetString
, prepending "Hello "
and printing the result using Print
from IOutput
:
let computation () = Do { let! text = sprintf "Hello %s" <?> liftDep fst getWord do! liftDep snd (print text) }
Sadly I have to make computation
into a function (or give F# some further hints by using it – see the gist) or else I will run into a Value restriction (I talked about it before). Anyway here is the output:
> computation () |> run config;; Hello World val it : unit = ()
If you like you can use (>>=)
instead:
let computation2 () = sprintf "Hello %s" <?> liftDep fst getWord >>= fmap (liftDep snd) print
This uses a helper function fmap
that is really just function-composition or the functor-mapping for 'c ->
if you want to be fancy:
let fmap (f : 'a -> 'b) (g : 'c -> 'a) : ('c -> 'b) = g >> f