I'm glad you liked the IL example. The best way to understand how expressions are desugared is probably to look at the spec (though it's a bit dense...).
There we can see that something like
C {
op1
op2
}
gets desugared as follows:
T([<CustomOperator>]op1; [<CustomOperator>]op2, [], fun v -> v, true) ⇒
CL([<CustomOperator>]op1; [<CustomOperator>]op2, [], C.Yield(), false) ⇒
CL([<CustomOperator>]op2, [], 〚 [<CustomOperator>]op1, C.Yield() |][], false) ⇒
CL([<CustomOperator>]op2, [], C.Op1(C.Yield()), false) ⇒
〚 [<CustomOperator>]op2, C.Op1(C.Yield()) 〛[] ⇒
C.Op2(C.Op1(C.Yield()))
As to why Yield()
is used rather than Zero
, it's because if there were variables in scope (e.g. because you used some lets
, or were in a for loop, etc.), then you would get Yield (v1,v2,...)
but Zero
clearly can't be used this way. Note that this means adding a superfluous let x = 1
into Tomas's lr
example will fail to compile, because Yield
will be called with an argument of type int
rather than unit
.
There's another trick which can help understand the compiled form of computation expressions, which is to (ab)use the auto-quotation support for computation expressions in F# 3. Just define a do-nothing Quote
member and make Run
just return its argument:
member __.Quote() = ()
member __.Run(q) = q
Now your computation expression will evaluate to the quotation of its desugared form. This can be pretty handy when debugging things.