Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combination monads in F#

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.

like image 799
Beau Trepp Avatar asked Jun 16 '16 07:06

Beau Trepp


3 Answers

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.

like image 124
Gus Avatar answered Oct 18 '22 19:10

Gus


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").

like image 21
scrwtp Avatar answered Oct 18 '22 20:10

scrwtp


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"]

like image 7
TheInnerLight Avatar answered Oct 18 '22 21:10

TheInnerLight