Creating an 'add' computation expression

66 views Asked by At

I'd like the example computation expression and values below to return 6. For some the numbers aren't yielding like I'd expect. What's the step I'm missing to get my result? Thanks!

type AddBuilder() =
    let mutable x = 0
    member _.Yield i = x <- x + i
    member _.Zero() = 0
    member _.Return() = x

let add = AddBuilder()

(* Compiler tells me that each of the numbers in add don't do anything
   and suggests putting '|> ignore' in front of each *)
let result = add { 1; 2; 3 }

(* Currently the result is 0 *)
printfn "%i should be 6" result

Note: This is just for creating my own computation expression to expand my learning. Seq.sum would be a better approach. I'm open to the idea that this example completely misses the value of computation expressions and is no good for learning.

1

There are 1 answers

1
Fyodor Soikin On BEST ANSWER

There is a lot wrong here.

First, let's start with mere mechanics.

In order for the Yield method to be called, the code inside the curly braces must use the yield keyword:

let result = add { yield 1; yield 2; yield 3 }

But now the compiler will complain that you also need a Combine method. See, the semantics of yield is that each of them produces a finished computation, a resulting value. And therefore, if you want to have more than one, you need some way to "glue" them together. This is what the Combine method does.

Since your computation builder doesn't actually produce any results, but instead mutates its internal variable, the ultimate result of the computation should be the value of that internal variable. So that's what Combine needs to return:

member _.Combine(a, b) = x

But now the compiler complains again: you need a Delay method. Delay is not strictly necessary, but it's required in order to mitigate performance pitfalls. When the computation consists of many "parts" (like in the case of multiple yields), it's often the case that some of them should be discarded. In these situation, it would be inefficient to evaluate all of them and then discard some. So the compiler inserts a call to Delay: it receives a function, which, when called, would evaluate a "part" of the computation, and Delay has the opportunity to put this function in some sort of deferred container, so that later Combine can decide which of those containers to discard and which to evaluate.

In your case, however, since the result of the computation doesn't matter (remember: you're not returning any results, you're just mutating the internal variable), Delay can just execute the function it receives to have it produce the side effects (which are - mutating the variable):

member _.Delay(f) = f ()

And now the computation finally compiles, and behold: its result is 6. This result comes from whatever Combine is returning. Try modifying it like this:

member _.Combine(a, b) = "foo"

Now suddenly the result of your computation becomes "foo".


And now, let's move on to semantics.

The above modifications will let your program compile and even produce expected result. However, I think you misunderstood the whole idea of the computation expressions in the first place.

The builder isn't supposed to have any internal state. Instead, its methods are supposed to manipulate complex values of some sort, some methods creating new values, some modifying existing ones. For example, the seq builder1 manipulates sequences. That's the type of values it handles. Different methods create new sequences (Yield) or transform them in some way (e.g. Combine), and the ultimate result is also a sequence.

In your case, it looks like the values that your builder needs to manipulate are numbers. And the ultimate result would also be a number.

So let's look at the methods' semantics.

The Yield method is supposed to create one of those values that you're manipulating. Since your values are numbers, that's what Yield should return:

member _.Yield x = x

The Combine method, as explained above, is supposed to combine two of such values that got created by different parts of the expression. In your case, since you want the ultimate result to be a sum, that's what Combine should do:

member _.Combine(a, b) = a + b

Finally, the Delay method should just execute the provided function. In your case, since your values are numbers, it doesn't make sense to discard any of them:

member _.Delay(f) = f()

And that's it! With these three methods, you can add numbers:

type AddBuilder() =
    member _.Yield x = x
    member _.Combine(a, b) = a + b
    member _.Delay(f) = f ()

let add = AddBuilder()

let result = add { yield 1; yield 2; yield 3 }

I think numbers are not a very good example for learning about computation expressions, because numbers lack the inner structure that computation expressions are supposed to handle. Try instead creating a maybe builder to manipulate Option<'a> values.

Added bonus - there are already implementations you can find online and use for reference.


1 seq is not actually a computation expression. It predates computation expressions and is treated in a special way by the compiler. But good enough for examples and comparisons.