Search code examples
f#eithercomputation-expression

Issue with yield in nested workflow


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


Solution

  • 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.