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
FSharpPlus
andFSharpPlus.Data
namespaces. - 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
run
function -OptionT.run
in 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
ao3
inAsync
usingasync.Return
- lift
ao4
to OptionT context usingOptionT.lift
function 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