Monad transformers
In previous posts we've looked into different Monad types. Today, we will show methods for working with combinations of monads.
General pattern
Let's say we have a monadic type Option<T> that is nested in Async<T>.
The resulting type is Async<Option<T>>.
We would like to be able to do some calculation on wrapped value.
Now we have to:
- Add FSharpPlus to our project.
- Open
FSharpPlusandFSharpPlus.Datanamespaces. - Wrap our value with monad transformer of nested monad -
OptionT<T>in our case. - CreateF#+ monad expression with our calculation logic.
- Materialize result with transformer
runfunction -OptionT.runin our case.
Concrete example - Asynchronous Option
Now, let's create an implementation of the pattern shown above.
let v1 : Async<Option<int>> = async { return Some 1 }
let v2 : Async<Option<int>> = async { return Some 2 }
let vResult =
monad {
let! x1 = OptionT v1
let! x2 = OptionT v2
return x1 + x2
} |> OptionT.run
|> Async.RunSynchronously
In a simple case like above we can just use F#+ lift/map functions:
let vResult' = lift2 (+) (OptionT v1) (OptionT v2)
|> OptionT.run
|> Async.RunSynchronously
let vResult'' = OptionT.map2 (+) (OptionT v1) (OptionT v2)
|> OptionT.run
|> Async.RunSynchronously
Working with values of different types
When dealing with transformers, we need all values to have the same type,
so that we can wrap them in OptionT transformer.
Let's say we have these values:
let ao1 = async { return Some 1 } // Async<Option<int>>
let ao2 = async { return Some 2 } // Async<Option<int>>
let ao3 = Some 3 // Option<int>, but not Async
let ao4 = async { return 4 } // Async<int>, but not Option
To make them all the same type, we will have to:
- wrap value
ao3inAsyncusingasync.Return - lift
ao4to OptionT context usingOptionT.liftfunction Then we will be able to make calculation using monad expression, as in a previous example.
let aoResult =
monad {
let! x1 = OptionT ao1
let! x2 = OptionT ao2
let! x3 = OptionT <| async.Return ao3
let! x4 = OptionT.lift ao4
return x1 + x2 + x3 + x4 // Or use `List.reduce (+) [x1;x2;x3;x4]`
} |> OptionT.run
|> Async.RunSynchronously
Another example - ResultT
Similar to Async Option:
type AsyncResult = Async<Result<int, string>>
let ar1: AsyncResult = async { return Ok 1 }
let ar2: AsyncResult = async { return Ok 2 }
let ar =
monad {
let! r1 = ResultT ar1
let! r2 = ResultT ar2
return r1 + r2
} |> ResultT.run
|> Async.RunSynchronously
let ar' = lift2 (+) (ResultT ar1) (ResultT ar2)
|> ResultT.run
|> Async.RunSynchronously
let ar'' = ResultT.map2 (+) (ResultT ar1) (ResultT ar2)
|> ResultT.run
|> Async.RunSynchronously
Async Result with effects and value passing
Now let's see a more interesting case of Result. Let's define couple of functions:
let r1 () = async {
printfn "r1 = %d" 1
return Ok 1 }
let r2 x = async {
let r2Val = x + 1
printfn "r2 = %d" r2Val
return Ok r2Val }
let r3 x = async {
let r3Val = x + 1
printfn "r3 = %d" r3Val
return Error "Problem in r3" }
let r4 () = async {
printfn "r4 is run"
return Ok () }
We wrap them in ResultT, so that we can bind returned values to variables,
or just execute effectful actions:
let aer =
monad {
let! v1 = ResultT <| (r1 ())
let! v2 = ResultT <| (r2 v1)
do! ResultT <| r3 v2
do! ResultT <| r4 ()
} |> ResultT.run
|> Async.RunSynchronously
Multiple monadic layers
Let's say we have three layers of monads:
type T1 = Async<Option<Result<int, string>>>
let v1' = async { return Some (Ok 1) } : T1
let v2' = async { return Some (Ok 2) } : T1
We can simplify dealing with nested transformers using function composition:
let combinedT = OptionT >> ResultT
let runCombined : ResultT<OptionT<T1>> -> T1 =
ResultT.run >> OptionT.run
Now we can execute in a straightforward way:
let result =
monad {
let! x1 = combinedT v1'
let! x2 = combinedT v2'
return x1 + x2
} |> runCombined
|> Async.RunSynchronously
Code
#r "nuget: FSharpPlus"
open FSharpPlus
open FSharpPlus.Data
// Async Option
let v1 : Async<Option<int>> = async { return Some 1 }
let v2 : Async<Option<int>> = async { return Some 2 }
let vResult =
monad {
let! x1 = OptionT v1
let! x2 = OptionT v2
return x1 + x2
} |> OptionT.run
|> Async.RunSynchronously
let vResult' = lift2 (+) (OptionT v1) (OptionT v2)
|> OptionT.run
|> Async.RunSynchronously
let vResult'' = OptionT.map2 (+) (OptionT v1) (OptionT v2)
|> OptionT.run
|> Async.RunSynchronously
// Working with values of different types
let ao1 = async { return Some 1 }
let ao2 = async { return Some 2 }
let ao3 = Some 3
let ao4 = async { return 4 }
let aoResult =
monad {
let! x1 = OptionT ao1
let! x2 = OptionT ao2
let! x3 = OptionT <| async.Return ao3
let! x4 = OptionT.lift ao4
return x1 + x2 + x3 + x4 // Or use `List.reduce (+) [x1;x2;x3;x4]`
} |> OptionT.run
|> Async.RunSynchronously
// Async Result
type AsyncResult = Async<Result<int, string>>
let ar1: AsyncResult = async { return Ok 1 }
let ar2: AsyncResult = async { return Ok 2 }
let ar =
monad {
let! r1 = ResultT ar1
let! r2 = ResultT ar2
return r1 + r2
} |> ResultT.run
|> Async.RunSynchronously
let ar' = lift2 (+) (ResultT ar1) (ResultT ar2)
|> ResultT.run
|> Async.RunSynchronously
let ar'' = ResultT.map2 (+) (ResultT ar1) (ResultT ar2)
|> ResultT.run
|> Async.RunSynchronously
// Async Result with effects and value passing
let r1 () = async {
printfn "r1 = %d" 1
return Ok 1 }
let r2 x = async {
let r2Val = x + 1
printfn "r2 = %d" r2Val
return Ok r2Val }
let r3 x = async {
let r3Val = x + 1
printfn "r3 = %d" r3Val
return Error "Problems in r3" }
let r4 () = async {
printfn "r4 is run"
return Ok () }
let aer =
monad {
let! v1 = ResultT <| (r1 ())
let! v2 = ResultT <| (r2 v1)
do! ResultT <| r3 v2
do! ResultT <| r4 ()
} |> ResultT.run
|> Async.RunSynchronously
// Multiple monadic layers
type T1 = Async<Option<Result<int, string>>>
let v1' = async { return Some (Ok 1) } : T1
let v2' = async { return Some (Ok 2) } : T1
let combinedT = OptionT >> ResultT
let runCombined : ResultT<OptionT<T1>> -> T1 =
ResultT.run >> OptionT.run
let result =
monad {
let! x1 = combinedT v1'
let! x2 = combinedT v2'
return x1 + x2
} |> runCombined
|> Async.RunSynchronously