Combination monads in F#
I know it's not generally considered idiomatic in F# but for the curious reader here's @TheInnerLight answer using F#+ :
#r @"FSharpPlus.1.0.0\lib\net45\FSharpPlus.dll"
open FSharpPlus
open FSharpPlus.Data
let divide5By = function
|0.0 -> Choice2Of2 "Divide by zero"
|x -> Choice1Of2 (5.0/x)
let eitherConv logSuccessF logFailF f v =
ChoiceT (
match f v with
| Choice1Of2 a -> Writer(Choice1Of2 a, ["Success: " + logSuccessF a])
| Choice2Of2 b -> Writer(Choice2Of2 b, ["ERROR: " + logFailF b] ))
let ew = monad {
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) = ew |> ChoiceT.run |> Writer.run
And of course this one works with any monoid.
This approach is basically the Haskell approach, the transformers work with any monad, and in the above code you can easily switch to OptionT
, replace Choice1Of2
with Some
and Choice2Of2
with None
and it will just work.
Personally I prefer to use this approach first, it's much easier to write and of course way shorter. Once I have the desired functionality I can customize my transformer, or leave as it is if it's good enough for what I'm trying to solve.
Writing a "combined" builder would be how you would do it in F# if you were to do it. This isn't a typical approach however, and certainly not a practical one.
In Haskell you need monad transformers because of how ubiquitous monads are in Haskell. This is not the case with F# - here computation workflows are a useful tool, but only a supplementary one. First and foremost - F# doesn't prohibit side effects, so one big reason for using monads is gone here.
The typical approach would be to identify the workflow that captures the essence of the computation you want to model (in your case it would seem to be the Either monad) and use other means for the rest of it - like threading a modified "environment" through the computation as a value or using side effects for logging (aka "logging framework").
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"]