Computation expressions and wrapper types

Using types to assist the workflow

In the previous post, we were introduced to the "maybe" workflow, which allowed us to hide the messiness of chaining together option types.

A typical use of the "maybe" workflow looked something like this:

let result = 
    maybe 
        {
        let! anInt = expression of Option<int>
        let! anInt2 = expression of Option<int>
        return anInt + anInt2 
        }

As we saw before, there is some apparently strange behavior going on here:

  • In the let! lines, the expression on the right of the equals is an int option, but the value on the left is just an int. The let! has "unwrapped" the option before binding it to the value.

  • And in the return line, the opposite occurs. The expression being returned is an int, but the value of the whole workflow (result) is an int option. That is, the return has "wrapped" the raw value back into an option.

We will follow up these observations in this post, and we will see that this leads to one of the major uses of computation expressions: namely, to implicitly unwrap and rewrap values that are stored in some sort of wrapper type.

Another example

Let's look at another example. Say that we are accessing a database, and we want to capture the result in a Success/Error union type, like this:

type DbResult<'a> = 
    | Success of 'a
    | Error of string

We then use this type in our database access methods. Here are some very simple stubs to give you an idea of how the DbResult type might be used:

Now let's say we want to chain these calls together. First get the customer id from the name, and then get the order for the customer id, and then get the product from the order.

Here's the most explicit way of doing it. As you can see, we have to have pattern matching at each step.

Really ugly code. And the top-level flow has been submerged in the error handling logic.

Computation expressions to the rescue! We can write one that handles the branching of Success/Error behind the scenes:

And with this workflow, we can focus on the big picture and write much cleaner code:

And if there are errors, the workflow traps them nicely and tells us where the error was, as in this example below:

The role of wrapper types in workflows

So now we have seen two workflows (the maybe workflow and the dbresult workflow), each with their own corresponding wrapper type (Option<T> and DbResult<T> respectively).

These are not just special cases. In fact, every computation expression must have an associated wrapper type. And the wrapper type is often designed specifically to go hand-in-hand with the workflow that we want to manage.

The example above demonstrates this clearly. The DbResult type we created is more than just a simple type for return values; it actually has a critical role in the workflow by "storing" the current state of the workflow, and whether it is succeeding or failing at each step. By using the various cases of the type itself, the dbresult workflow can manage the transitions for us, hiding them from view and enabling us to focus on the big picture.

We'll learn how to design a good wrapper type later in the series, but first let's look at how they are manipulated.

Bind and Return and wrapper types

Let's look again at the definition of the Bind and Return methods of a computation expression.

We'll start off with the easy one, Return. The signature of Return as documented on MSDN is just this:

In other words, for some type T, the Return method just wraps it in the wrapper type.

Note: In signatures, the wrapper type is normally called M, so M<int> is the wrapper type applied to int and M<string> is the wrapper type applied to string, and so on.

And we've seen two examples of this usage. The maybe workflow returns a Some, which is an option type, and the dbresult workflow returns Success, which is part of the DbResult type.

Now let's look at Bind. The signature of Bind is this:

It looks complicated, so let's break it down. It takes a tuple M<'T> * ('T -> M<'U>) and returns a M<'U>, where M<'U> means the wrapper type applied to type U.

The tuple in turn has two parts:

  • M<'T> is a wrapper around type T, and

  • 'T -> M<'U> is a function that takes a unwrapped T and creates a wrapped U.

In other words, what Bind does is:

  • Take a wrapped value.

  • Unwrap it and do any special "behind the scenes" logic.

  • Then, optionally apply the function to the unwrapped value to create a new wrapped value.

  • Even if the function is not applied, Bind must still return a wrapped U.

With this understanding, here are the Bind methods that we have seen already:

Look over this code and make sure that you understand why these methods do indeed follow the pattern described above.

Finally, a picture is always useful. Here is a diagram of the various types and functions:

diagram of bind
  • For Bind, we start with a wrapped value (m here), unwrap it to a raw value of type T, and then (maybe) apply the function f to it to get a wrapped value of type U.

  • For Return, we start with a value (x here), and simply wrap it.

The type wrapper is generic

Note that all the functions use generic types (T and U) other than the wrapper type itself, which must be the same throughout. For example, there is nothing stopping the maybe binding function from taking an int and returning a Option<string>, or taking a string and then returning an Option<bool>. The only requirement is that it always return an Option<something>.

To see this, we can revisit the example above, but rather than using strings everywhere, we will create special types for the customer id, order id, and product id. This means that each step in the chain will be using a different type.

We'll start with the types again, this time defining CustomerId, etc.

The code is almost identical, except for the use of the new types in the Success line.

Here's the long-winded version again.

There are a couple of changes worth discussing:

  • First, the printfn at the bottom uses the "%A" format specifier rather than "%s". This is required because the ProductId type is a union type now.

  • More subtly, there seems to be unnecessary code in the error lines. Why write | Error e -> Error e? The reason is that the incoming error that is being matched against is of type DbResult<CustomerId> or DbResult<OrderId>, but the return value must be of type DbResult<ProductId>. So, even though the two Errors look the same, they are actually of different types.

Next up, the builder, which hasn't changed at all except for the | Error e -> Error e line.

Finally, we can use the workflow as before.

At each line, the returned value is of a different type (DbResult<CustomerId>,DbResult<OrderId>, etc), but because they have the same wrapper type in common, the bind works as expected.

And finally, here's the workflow with an error case.

Composition of computation expressions

We've seen that every computation expression must have an associated wrapper type. This wrapper type is used in both Bind and Return, which leads to a key benefit:

  • the output of a Return can be fed to the input of a Bind

In other words, because a workflow returns a wrapper type, and because let! consumes a wrapper type, you can put a "child" workflow on the right hand side of a let! expression.

For example, say that you have a workflow called myworkflow. Then you can write the following:

Or you can even "inline" them, like this:

If you have used the async workflow, you probably have done this already, because an async workflow typically contains other asyncs embedded in it:

Introducing "ReturnFrom"

We have been using return as a way of easily wrapping up an unwrapped return value.

But sometimes we have a function that already returns a wrapped value, and we want to return it directly. return is no good for this, because it requires an unwrapped type as input.

The solution is a variant on return called return!, which takes a wrapped type as input and returns it.

The corresponding method in the "builder" class is called ReturnFrom. Typically the implementation just returns the wrapped type "as is" (although of course, you can always add extra logic behind the scenes).

Here is a variant on the "maybe" workflow to show how it can be used:

And here it is in use, compared with a normal return.

For a more realistic example, here is return! used in conjunction with divideBy:

Summary

This post introduced wrapper types and how they related to Bind, Return and ReturnFrom, the core methods of any builder class.

In the next post, we'll continue to look at wrapper types, including using lists as wrapper types.

Last updated

Was this helpful?