Implementing a builder: The rest of the standard methods
Implementing While, Using, and exception handling
Last updated
Was this helpful?
Implementing While, Using, and exception handling
Last updated
Was this helpful?
We're coming into the home stretch now. There are only a few more builder methods that need to be covered, and then you will be ready to tackle anything!
These methods are:
While
for repetition.
TryWith
and TryFinally
for handling exceptions.
Use
for managing disposables
Remember, as always, that not all methods need to be implemented. If While
is not relevant to you, don't bother with it.
One important note before we get started: all the methods discussed here rely on being used. If you are not using delay functions, then none of the methods will give the expected results.
We all know what "while" means in normal code, but what does it mean in the context of a computation expression? To understand, we have to revisit the concept of continuations again.
In previous posts, we saw that a series of expressions is converted into a chain of continuations like this:
And this is the key to understanding a "while" loop -- it can be expanded in the same way.
First, some terminology. A while loop has two parts:
There is a test at the top of the "while" loop which is evaluated each time to determine whether the body should be run. When it evaluates to false, the while loop is "exited". In computation expressions, the test part is known as the "guard".
The test function has no parameters, and returns a bool, so its signature is unit -> bool
, of course.
And there is the body of the "while" loop, evaluated each time until the "while" test fails. In computation expressions, this is a delay function that evaluates to a wrapped value. Since the body of the while loop is always the same, the same function is evaluated each time.
The body function has no parameters, and returns nothing, and so its signature is just unit -> wrapped unit
.
With this in place, we can create pseudo-code for a while loop using continuations:
One question that is immediately apparent is: what should be returned when the while loop test fails? Well, we have seen this before with if..then..
, and the answer is of course to use the Zero
value.
The next thing is that the body()
result is being discarded. Yes, it is a unit function, so there is no value to return, but even so, in our expressions, we want to be able to hook into this so we can add behavior behind the scenes. And of course, this calls for using the Bind
function.
So here is a revised version of the pseudo-code, using Zero
and Bind
:
In this case, the continuation function passed into Bind
has a unit parameter, because the body
function does not have a value.
Finally, the pseudo-code can be simplified by collapsing it into a recursive function like this:
And indeed, this is the standard "boiler-plate" implementation for While
in almost all builder classes.
It is a subtle but important point that the value of Zero
must be chosen properly. In previous posts, we saw that we could set the value for Zero
to be None
or Some ()
depending on the workflow. For While
to work however, the Zero
must be set to Some ()
and not None
, because passing None
into Bind
will cause the whole thing to aborted early.
Also note that, although this is a recursive function, we didn't need the rec
keyword. It is only needed for standalone functions that are recursive, not methods.
Let's look at it being used in the trace
builder. Here's the complete builder class, with the While
method:
If you look at the signature for While
, you will see that the body
parameter is unit -> unit option
, that is, a delayed function. As noted above, if you don't implement Delay
properly, you will get unexpected behavior and cryptic compiler errors.
And here is a simple loop using a mutable value that is incremented each time round.
Exception handling is implemented in a similar way.
If we look at a try..with
expression for example, it has two parts:
There is the body of the "try", evaluated once. In a computation expressions, this will be a delayed function that evaluates to a wrapped value. The body function has no parameters, and so its signature is just unit -> wrapped type
.
The "with" part handles the exception. It has an exception as a parameters, and returns the same type as the "try" part, so its signature is exception -> wrapped type
.
With this in place, we can create pseudo-code for the exception handler:
And this maps exactly to a standard implementation:
As you can see, it is common to use pass the returned value through ReturnFrom
so that it gets the same treatment as other wrapped values.
Here is an example snippet to test how the handling works:
try..finally
is very similar to try..with
.
There is the body of the "try", evaluated once. The body function has no parameters, and so its signature is unit -> wrapped type
.
The "finally" part is always called. It has no parameters, and returns a unit, so its signature is unit -> unit
.
Just as with try..with
, the standard implementation is obvious.
Another little snippet:
The final method to implement is Using
. This is the builder method for implementing the use!
keyword.
This is what the MSDN documentation says about use!
:
is translated to:
In other words, the use!
keyword triggers both a Bind
and a Using
. First a Bind
is done to unpack the wrapped value, and then the unwrapped disposable is passed into Using
to ensure disposal, with the continuation function as the second parameter.
Implementing this is straightforward. Similar to the other methods, we have a body, or continuation part, of the "using" expression, which is evaluated once. This body function has a "disposable" parameter, and so its signature is #IDisposable -> wrapped type
.
Of course we want to ensure that the disposable value is always disposed no matter what, so we need to wrap the call to the body function in a TryFinally
.
Here's a standard implementation:
Notes:
The parameter to TryFinally
is a unit -> wrapped
, with a unit as the first parameter, so we created a delayed version of the body that is passed in.
Disposable is a class, so it could be null
, and we have to handle that case specially. Otherwise we just dispose it in the "finally" continuation.
Here's a demonstration of Using
in action. Note that the makeResource
makes a wrapped disposable. If it wasn't wrapped, we wouldn't need the special use!
and could just use a normal use
instead.
Finally, we can revisit how For
is implemented. In the previous examples, For
took a simple list parameter. But with Using
and While
under our belts, we can change it to accept any IEnumerable<_>
or sequence.
Here's the standard implementation for For
now:
After all this discussion, the code seems quite tiny now. And yet this builder implements every standard method, uses delayed functions. A lot of functionality in a just a few lines!