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"]
Async<'T>in lieu of Haskell'sIO<'T>as @MarkSeemann points out in his linked blog post. - Gus