layout: post
title: “Implementing a builder: Delay and Run”
description: “Controlling when functions execute”
nav: thinking-functionally
seriesId: “Computation Expressions”

seriesOrder: 8

In the last few posts we have covered all the basic methods (Bind, Return, Zero, and Combine) needed to create your own computation expression builder. In this post, we’ll look at some of the extra features needed to make the workflow more efficient, by controlling when expressions get evaluated.

The problem: avoiding unnecessary evaluations

Let’s say that we have created a “maybe” style workflow as before. But this time we want to use the “return” keyword to return early and stop any more processing being done.

Here is our complete builder class. The key method to look at is Combine, in which we simply ignore any secondary expressions after the first return.

  1. type TraceBuilder() =
  2. member this.Bind(m, f) =
  3. match m with
  4. | None ->
  5. printfn "Binding with None. Exiting."
  6. | Some a ->
  7. printfn "Binding with Some(%A). Continuing" a
  8. Option.bind f m
  9. member this.Return(x) =
  10. printfn "Return an unwrapped %A as an option" x
  11. Some x
  12. member this.Zero() =
  13. printfn "Zero"
  14. None
  15. member this.Combine (a,b) =
  16. printfn "Returning early with %A. Ignoring second part: %A" a b
  17. a
  18. member this.Delay(f) =
  19. printfn "Delay"
  20. f()
  21. // make an instance of the workflow
  22. let trace = new TraceBuilder()

Let’s see how it works by printing something, returning, and then printing something else:

  1. trace {
  2. printfn "Part 1: about to return 1"
  3. return 1
  4. printfn "Part 2: after return has happened"
  5. } |> printfn "Result for Part1 without Part2: %A"

The debugging output should look something like the following, which I have annotated:

  1. // first expression, up to "return"
  2. Delay
  3. Part 1: about to return 1
  4. Return an unwrapped 1 as an option
  5. // second expression, up to last curly brace.
  6. Delay
  7. Part 2: after return has happened
  8. Zero // zero here because no explicit return was given for this part
  9. // combining the two expressions
  10. Returning early with Some 1. Ignoring second part: <null>
  11. // final result
  12. Result for Part1 without Part2: Some 1

We can see a problem here. The “Part 2: after return” was printed, even though we were trying to return early.

Why? Well I’ll repeat what I said in the last post: return and yield do not generate an early return from a computation expression. The entire computation expression, all the way to the last curly brace, is always evaluated and results in a single value.

This is a problem, because you might get unwanted side effects (such as printing a message in this case) and your code is doing something unnecessary, which might cause performance problems.

So, how can we avoid evaluating the second part until we need it?

Introducing “Delay”

The answer to the question is straightforward — simply wrap part 2 of the expression in a function and only call this function when needed, like this.

  1. let part2 =
  2. fun () ->
  3. printfn "Part 2: after return has happened"
  4. // do other stuff
  5. // return Zero
  6. // only evaluate if needed
  7. if needed then
  8. let result = part2()

Using this technique, part 2 of the computation expression can be processed completely, but because the expression returns a function, nothing actually happens until the function is called.
But the Combine method will never call it, and so the code inside it does not run at all.

And this is exactly what the Delay method is for. Any result from Return or Yield is immediately wrapped in a “delay” function like this, and then you can choose whether to run it or not.

Let’s change the builder to implement a delay:

  1. type TraceBuilder() =
  2. // other members as before
  3. member this.Delay(funcToDelay) =
  4. let delayed = fun () ->
  5. printfn "%A - Starting Delayed Fn." funcToDelay
  6. let delayedResult = funcToDelay()
  7. printfn "%A - Finished Delayed Fn. Result is %A" funcToDelay delayedResult
  8. delayedResult // return the result
  9. printfn "%A - Delaying using %A" funcToDelay delayed
  10. delayed // return the new function

As you can see, the Delay method is given a function to execute. Previously, we executed it immediately. What we’re doing now is wrapping this function in another function and returning the delayed function instead. I have added a number of trace statements before and after the function is wrapped.

If you compile this code, you can see that the signature of Delay has changed. Before the change, it returned a concrete value (an option in this case), but now it returns a function.

  1. // signature BEFORE the change
  2. member Delay : f:(unit -> 'a) -> 'a
  3. // signature AFTER the change
  4. member Delay : f:(unit -> 'b) -> (unit -> 'b)

By the way, we could have implemented Delay in a much simpler way, without any tracing, just by returning the same function that was passed in, like this:

  1. member this.Delay(f) =
  2. f

Much more concise! But in this case, I wanted to add some detailed tracing information as well.

Now let’s try again:

  1. trace {
  2. printfn "Part 1: about to return 1"
  3. return 1
  4. printfn "Part 2: after return has happened"
  5. } |> printfn "Result for Part1 without Part2: %A"

Uh-oh. This time nothing happens at all! What went wrong?

If we look at the output we see this:


Result for Part1 without Part2: <fun:Delay@84-5>

Hmmm. The output of the whole trace expression is now a function, not an option. Why? Because we created all these delays, but we never “undelayed” them by actually calling the function!

One way to do this is to assign the output of the computation expression to a function value, say f, and then evaluate it.

  1. let f = trace {
  2. printfn "Part 1: about to return 1"
  3. return 1
  4. printfn "Part 2: after return has happened"
  5. }
  6. f() |> printfn "Result for Part1 without Part2: %A"

This works as expected, but is there a way to do this from inside the computation expression itself? Of course there is!

Introducing “Run”

The Run method exists for exactly this reason. It is called as the final step in the process of evaluating a computation expression, and can be used to undo the delay.

Here’s an implementation:

  1. type TraceBuilder() =
  2. // other members as before
  3. member this.Run(funcToRun) =
  4. printfn "%A - Run Start." funcToRun
  5. let runResult = funcToRun()
  6. printfn "%A - Run End. Result is %A" funcToRun runResult
  7. runResult // return the result of running the delayed function

Let’s try one more time:

  1. trace {
  2. printfn "Part 1: about to return 1"
  3. return 1
  4. printfn "Part 2: after return has happened"
  5. } |> printfn "Result for Part1 without Part2: %A"

And the result is exactly what we wanted. The first part is evaluated, but the second part is not. And the result of the entire computation expression is an option, not a function.

When is delay called?

The way that Delay is inserted into the workflow is straightforward, once you understand it.

  • The bottom (or innermost) expression is delayed.
  • If this is combined with a prior expression, the output of Combine is also delayed.
  • And so on, until the final delay is fed into Run.

Using this knowledge, let’s review what happened in the example above:

  • The first part of the expression is the print statement plus return 1.
  • The second part of the expression is the print statement without an explicit return, which means that Zero() is called
  • The None from the Zero is fed into Delay, resulting in a “delayed option”, that is, a function that will evaluate to an option when called.
  • The option from part 1 and the delayed option from part 2 are combined in Combine and the second one is discarded.
  • The result of the combine is turned into another “delayed option”.
  • Finally, the delayed option is fed to Run, which evaluates it and returns a normal option.

Here is a diagram that represents this process visually:

Delay

If we look at the debug trace for the example above, we can see in detail what happened. It’s a little confusing, so I have annotated it.
Also, it helps to remember that working down this trace is the same as working up from the bottom of the diagram above, because the outermost code is run first.

  1. // delaying the overall expression (the output of Combine)
  2. <fun:clo@160-66> - Delaying using <fun:delayed@141-3>
  3. // running the outermost delayed expression (the output of Combine)
  4. <fun:delayed@141-3> - Run Start.
  5. <fun:clo@160-66> - Starting Delayed Fn.
  6. // the first expression results in Some(1)
  7. Part 1: about to return 1
  8. Return an unwrapped 1 as an option
  9. // the second expression is wrapped in a delay
  10. <fun:clo@162-67> - Delaying using <fun:delayed@141-3>
  11. // the first and second expressions are combined
  12. Combine. Returning early with Some 1. Ignoring <fun:delayed@141-3>
  13. // overall delayed expression (the output of Combine) is complete
  14. <fun:clo@160-66> - Finished Delayed Fn. Result is Some 1
  15. <fun:delayed@141-3> - Run End. Result is Some 1
  16. // the result is now an Option not a function
  17. Result for Part1 without Part2: Some 1

“Delay” changes the signature of “Combine”

When Delay is introduced into the pipeline like this, it has an effect on the signature of Combine.

When we originally wrote Combine we were expecting it to handle options. But now it is handling the output of Delay, which is a function.

We can see this if we hard-code the types that Combine expects, with int option type annotations like this:

  1. member this.Combine (a: int option,b: int option) =
  2. printfn "Returning early with %A. Ignoring %A" a b
  3. a

If this is done, we get an compiler error in the “return” expression:

  1. trace {
  2. printfn "Part 1: about to return 1"
  3. return 1
  4. printfn "Part 2: after return has happened"
  5. } |> printfn "Result for Part1 without Part2: %A"

The error is:

  1. error FS0001: This expression was expected to have type
  2. int option
  3. but here has type
  4. unit -> 'a

In other words, the Combine is being passed a delayed function (unit -> 'a), which doesn’t match our explicit signature.

So what happens when we do want to combine the parameters, but they are passed in as a function instead of as a simple value?

The answer is straightforward: just call the function that was passed in to get the underlying value.

Let’s demonstrate that using the adding example from the previous post.

  1. type TraceBuilder() =
  2. // other members as before
  3. member this.Combine (m,f) =
  4. printfn "Combine. Starting second param %A" f
  5. let y = f()
  6. printfn "Combine. Finished second param %A. Result is %A" f y
  7. match m,y with
  8. | Some a, Some b ->
  9. printfn "combining %A and %A" a b
  10. Some (a + b)
  11. | Some a, None ->
  12. printfn "combining %A with None" a
  13. Some a
  14. | None, Some b ->
  15. printfn "combining None with %A" b
  16. Some b
  17. | None, None ->
  18. printfn "combining None with None"
  19. None

In this new version of Combine, the second parameter is now a function, not an int option. So to combine them, we must first evaluate the function before doing the combination logic.

If we test this out:

  1. trace {
  2. return 1
  3. return 2
  4. } |> printfn "Result for return then return: %A"

We get the following (annotated) trace:

  1. // entire expression is delayed
  2. <fun:clo@318-69> - Delaying using <fun:delayed@295-6>
  3. // entire expression is run
  4. <fun:delayed@295-6> - Run Start.
  5. // delayed entire expression is run
  6. <fun:clo@318-69> - Starting Delayed Fn.
  7. // first return
  8. Returning a unwrapped 1 as an option
  9. // delaying second return
  10. <fun:clo@319-70> - Delaying using <fun:delayed@295-6>
  11. // combine starts
  12. Combine. Starting second param <fun:delayed@295-6>
  13. // delayed second return is run inside Combine
  14. <fun:clo@319-70> - Starting Delayed Fn.
  15. Returning a unwrapped 2 as an option
  16. <fun:clo@319-70> - Finished Delayed Fn. Result is Some 2
  17. // delayed second return is complete
  18. Combine. Finished second param <fun:delayed@295-6>. Result is Some 2
  19. combining 1 and 2
  20. // combine is complete
  21. <fun:clo@318-69> - Finished Delayed Fn. Result is Some 3
  22. // delayed entire expression is complete
  23. <fun:delayed@295-6> - Run End. Result is Some 3
  24. // Run is complete
  25. // final result is printed
  26. Result for return then return: Some 3

Understanding the type constraints

Up to now, we have used only our “wrapped type” (e.g. int option) and the delayed version (e.g. unit -> int option) in the implementation of our builder.

But in fact we can use other types if we like, subject to certain constraints.
In fact, understanding exactly what the type constraints are in a computation expression can clarify how everything fits together.

For example, we have seen that:

  • The output of Return is passed into Delay, so they must have compatible types.
  • The output of Delay is passed into the second parameter of Combine.
  • The output of Delay is also passed into Run.

But the output of Return does not have to be our “public” wrapped type. It could be an internally defined type instead.

Delay

Similarly, the delayed type does not have to be a simple function, it could be any type that satisfies the constraints.

So, given a simple set of return expressions, like this:

  1. trace {
  2. return 1
  3. return 2
  4. return 3
  5. } |> printfn "Result for return x 3: %A"

Then a diagram that represents the various types and their flow would look like this:

Delay

And to prove that this is valid, here is an implementation with distinct types for Internal and Delayed:

  1. type Internal = Internal of int option
  2. type Delayed = Delayed of (unit -> Internal)
  3. type TraceBuilder() =
  4. member this.Bind(m, f) =
  5. match m with
  6. | None ->
  7. printfn "Binding with None. Exiting."
  8. | Some a ->
  9. printfn "Binding with Some(%A). Continuing" a
  10. Option.bind f m
  11. member this.Return(x) =
  12. printfn "Returning a unwrapped %A as an option" x
  13. Internal (Some x)
  14. member this.ReturnFrom(m) =
  15. printfn "Returning an option (%A) directly" m
  16. Internal m
  17. member this.Zero() =
  18. printfn "Zero"
  19. Internal None
  20. member this.Combine (Internal x, Delayed g) : Internal =
  21. printfn "Combine. Starting %A" g
  22. let (Internal y) = g()
  23. printfn "Combine. Finished %A. Result is %A" g y
  24. let o =
  25. match x,y with
  26. | Some a, Some b ->
  27. printfn "Combining %A and %A" a b
  28. Some (a + b)
  29. | Some a, None ->
  30. printfn "combining %A with None" a
  31. Some a
  32. | None, Some b ->
  33. printfn "combining None with %A" b
  34. Some b
  35. | None, None ->
  36. printfn "combining None with None"
  37. None
  38. // return the new value wrapped in a Internal
  39. Internal o
  40. member this.Delay(funcToDelay) =
  41. let delayed = fun () ->
  42. printfn "%A - Starting Delayed Fn." funcToDelay
  43. let delayedResult = funcToDelay()
  44. printfn "%A - Finished Delayed Fn. Result is %A" funcToDelay delayedResult
  45. delayedResult // return the result
  46. printfn "%A - Delaying using %A" funcToDelay delayed
  47. Delayed delayed // return the new function wrapped in a Delay
  48. member this.Run(Delayed funcToRun) =
  49. printfn "%A - Run Start." funcToRun
  50. let (Internal runResult) = funcToRun()
  51. printfn "%A - Run End. Result is %A" funcToRun runResult
  52. runResult // return the result of running the delayed function
  53. // make an instance of the workflow
  54. let trace = new TraceBuilder()

And the method signatures in the builder class methods look like this:

  1. type Internal = | Internal of int option
  2. type Delayed = | Delayed of (unit -> Internal)
  3. type TraceBuilder =
  4. class
  5. new : unit -> TraceBuilder
  6. member Bind : m:'a option * f:('a -> 'b option) -> 'b option
  7. member Combine : Internal * Delayed -> Internal
  8. member Delay : funcToDelay:(unit -> Internal) -> Delayed
  9. member Return : x:int -> Internal
  10. member ReturnFrom : m:int option -> Internal
  11. member Run : Delayed -> int option
  12. member Zero : unit -> Internal
  13. end

Creating this artifical builder is overkill of course, but the signatures clearly show how the various methods fit together.

Summary

In this post, we’ve seen that:

  • You need to implement Delay and Run if you want to delay execution within a computation expression.
  • Using Delay changes the signature of Combine.
  • Delay and Combine can use internal types that are not exposed to clients of the computation expression.

The next logical step is wanting to delay execution outside a computation expression until you are ready, and that will be the topic on the next but one post.
But first, we’ll take a little detour to discuss method overloads.