I'm trying to write my own Either builder as part of my quest to learn computation expressions in f#, but I have hit a wall with what I think is issue with Combine method. My code so far:
type Result<'a> =
| Failure
| Success of 'a
type EitherBuilder() =
member this.Bind(m,f) =
match m with
| Failure -> Failure
| Success(x) -> f x
member this.Yield x =
Success(x)
member this.YieldFrom x =
x
member this.Combine(a,b) =
match a with
| Success(_) -> a
| Failure -> b()
member this.Delay y =
fun () -> y()
member this.Run(func) =
func()
With this code I test the Combine with two tests:
let either = new EitherBuilder()
...
testCase "returns success from 3 yields" <|
fun _ ->
let value = either {
yield! Failure
yield 4
yield! Failure
}
value |> should equal (Success(4))
testCase "returns success with nested workflows" <|
fun _ ->
let value = either {
let! x = either {
yield! Failure
}
yield 5
}
value |> should equal (Success(5))
The first test passes, as I would expect, but the second test fails with following message:
Exception thrown: 'NUnit.Framework.AssertionException' in nunit.framework.dll either tests/returns success with nested workflows: Failed: Expected:
<Success 5>
But was:<Failure>
I don't get it. The x
is not yielded, so why does it influence my parent workflow? If I move let! below yield the test passes. I'm staring at my Combine implementation and it looks for me that for Failure*Success
pair the actual order of arguments would not influence the result, but yet it seems like it does
do!
and let!
clauses within the expression get desugared to Bind
calls. This means that your Bind
is called when you do let! x = ...
.
More specifically, your second example gets desugared into the following:
let value =
either.Bind(
either.YieldFrom Failure, // yield! Failure
fun x -> // let! x =
either.Yield 5 // yield 5
)
So it never even gets to yield 5
- the computation stops at let! x =
.
In order for the inner computation to "never become part" of the outer one, just use let
(without the bang):
let value = either {
let x = either {
yield! Failure
}
yield 5
}
This will correctly return Success 5
.