I'm trying to get my head around monads in F#, and am looking for an example of composing them.
In haskell it looks like you would use Monad Transformers but in F# it appears that you would create your own computation expression builder.
I can get behind that, but are there any examples of some combinations of the standard monads and how to use them?
I'm particularly interested in combining Reader, Writer and Either to build functions that take in an environment, tweak it, and then using Writer return the changes to the environment that took place. Either would be used to differentiate between successes and failures.
For now, it would be great to obtain a example of an EitherWriter computation expression which produces a value+log or an error.
I'm going to show how you can create an EitherWriter, there are two ways you can go about building one of these depending on how you order the Either
and the Writer
but I'm going to show the example that seems to most resemble your desired workflow.
I'm also going to simplify the writer such that it only logs to a string list
. A fuller writer implementation would use mempty
and mappend
to abstract over appropriate types.
Type definition:
type EitherWriter<'a,'b> = EWriter of string list * Choice<'a,'b>
Basic functions:
let runEitherWriter = function
|EWriter (st, v) -> st, v
let return' x = EWriter ([], Choice1Of2 x)
let bind x f =
let (st, v) = runEitherWriter x
match v with
|Choice1Of2 a ->
match runEitherWriter (f a) with
|st', Choice1Of2 a -> EWriter(st @ st', Choice1Of2 a)
|st', Choice2Of2 b -> EWriter(st @ st', Choice2Of2 b)
|Choice2Of2 b -> EWriter(st, Choice2Of2 b)
I like to define these in a standalone module and then I can use them directly or reference them to create the computation expression. Again, I'm going to keep it simple and just do the most basic usable implementation:
type EitherWriterBuilder() =
member this.Return x = return' x
member this.ReturnFrom x = x
member this.Bind(x,f) = bind x f
member this.Zero() = return' ()
let eitherWriter = EitherWriterBuilder()
Is any of this practical?
F# for fun and profit has some great information about railway oriented programming and the advantages that it brings compared to competing methods.
These examples are based on a custom Result<'TSuccess,'TFailure>
but, of course, they could equally be applied using F#'s built-in Choice<'a,'b>
type.
While we are likely to encounter code expressed in this railway-oriented form, we are far less likely to encounter code pre-written to be usable directly with an EitherWriter
. The practicality of this method therefore depends on easy conversion from simple success/failure code into something compatible with the monad presented above.
Here is an example of a success/fail function:
let divide5By = function
|0.0 -> Choice2Of2 "Divide by zero"
|x -> Choice1Of2 (5.0/x)
This function just divides 5 by a supplied number. If that number is non-zero it returns a success containing the result, if the supplied number is zero, it returns a failure telling us we've tried to divide by zero.
We now need a helper function to transform functions like this into something usable within our EitherWriter
. A function that could do that is this:
let eitherConv logSuccessF logFailF f =
fun v ->
match f v with
|Choice1Of2 a -> EWriter(["Success: " + logSuccessF a], Choice1Of2 a)
|Choice2Of2 b -> EWriter(["ERROR: " + logFailF b], Choice2Of2 b)
It takes a function describing how to log successes, a function describing how to log failures and a binding function for the Either
monad and it returns a binding function for the EitherWriter
monad.
We could use it like this:
let ew = eitherWriter {
let! x = eitherConv (sprintf "%f") (sprintf "%s") divide5By 6.0
let! y = eitherConv (sprintf "%f") (sprintf "%s") divide5By 3.0
let! z = eitherConv (sprintf "%f") (sprintf "%s") divide5By 0.0
return (x, y, z)
}
let (log, _) = runEitherWriter ew
printfn "%A" log
It then returns:
["Success: 0.833333"; "Success: 1.666667"; "ERROR: Divide by zero"]