Computational Expressions: Introduction


By popular demand, we'll talk about the mysteries of computational expressions, what they are and how they can be used in practice (and I'll try to avoid the forbidden M word).

In this series of articles, you will learn what computational expressions are, how to create them, and also master several common patterns associated with them. Along the way, we'll also learn about continuations, the bind function, wrapper types, and more.

Introduction

It seems that computational expressions have a reputation for being abstruse and difficult to understand.

On the one hand, they are quite easy to use. Anyone who has written enough code in F# has probably used standard constructs such as seq{...} or async{...}.

But how can you create a new, similar design? How do they work behind the scenes?

Unfortunately, many explanations seem to make the whole thing much more confusing. It seems that there is a kind of mental bridge that you must cross. To those on the other side, everything seems obvious, but to those on this side, they are confused.

If we ask for help official MSDN documentationwe will find that it is accurate, but rather useless for a beginner.

Specifically, she says that when we see code like this in a computational expression:

{| let! pattern = expr in cexpr |}

then this is actually syntactic sugar for a call like this:

builder.Bind(expr, (fun pattern -> {| cexpr |}))

But… what is it and what is it for?

I hope that by the end of the series, documentation such as the example above will become apparent. Don't believe me? Read on!

Computational Expressions in Practice

Before diving into the depths of computational expressions, let's look at a few trivial examples that show the same code with and without computational expressions.

Let's start with a simple example. Let's imagine that we have a code and we want to log every step.
We write a small logging function and call it after each calculation.

let log p = printfn "expression is %A" p

let loggedWorkflow =
    let x = 42
    log x
    let y = 43
    log y
    let z = x + y
    log z
    //return
    z

If you run this program you will see:

expression is 42
expression is 43
expression is 85

It's not difficult at all.

But it’s annoying that you have to call the logging function all the time. Is there a way to hide this call?

It's good that you asked… Calculation expressions will help solve the problem. Here's the code that will do the same thing.

First, let's define a new type LoggingBuilder:

type LoggingBuilder() =
    let log p = printfn "expression is %A" p

    member this.Bind(x, f) =
        log x
        f x

    member this.Return(x) =
        x

Don't worry about the mysterious ones just yet Bind And Return — we will definitely return to them later.

notethat “builder” or “builder” in the context of computational expressions is not the same as the object-oriented pattern Builderwhich is used to create complex objects.

We will then instantiate the declared type, in our case logger.

let logger = new LoggingBuilder()

Now, having loggerwe can rewrite the original example like this:

let loggedWorkflow =
    logger
        {
        let! x = 42
        let! y = 43
        let! z = x + y
        return z
        }

By running the code, you will see the same thing on the screen, but, as you may have noticed, the use of the construct logger{...} allowed us to get rid of repetitive code.

Safe division

Now let's deal with one bearded story.

Let's imagine that we need to divide several numbers by each other, but one of them may be equal to zero. How do we handle a possible error? You can throw exceptions, but it will look quite ugly. Rather, the type would fit here option.

First we need to write a helper function that divides numbers by each other and returns a result like int option. If everything went well, we get some (Some) value, and if not, we get nothing (None).

We then chain the divisions and check the result after each division, continuing only if successful.

Let's write the auxiliary function first, and then the main code.

let divideBy bottom top =
    if bottom = 0
    then None
    else Some(top/bottom)

Please note that we put the divisor first in the list of parameters. This will allow us to write expressions in the form 12 |> divideBy 3thanks to which we can combine them into a chain.

Now let's use our function. Here is the code where the initial value is sequentially divided by three numbers.

let divideByWorkflow init x y z =
    let a = init |> divideBy x
    match a with
    | None -> None  // останавливаемся
    | Some a' ->    // продолжаем
        let b = a' |> divideBy y
        match b with
        | None -> None  // останавливаемся
        | Some b' ->    // продолжаем
            let c = b' |> divideBy z
            match c with
            | None -> None  // останавливаемся
            | Some c' ->    // продолжаем
                // возвращаем результат
                Some c'

And here we use it:

let good = divideByWorkflow 12 3 2 1
let bad = divideByWorkflow 12 3 0 1

An incorrect chain of divisors will cause an error in the third step and return None as a result of the entire expression.

note that whole expression must also be of type int option. It cannot be just an integer, because then it is not clear what it should be equal to in case of an error. As you can see, the type we use inside the chain is − option is the same type we will get in the end. Remember this point – we will return to it later.

In any case, all these endless checks and branches look truly terrible! Can computational expressions get rid of them?

Let's define a new type again (MaybeBuilder) and create an instance of it (maybe).

type MaybeBuilder() =

    member this.Bind(x, f) =
        match x with
        | None -> None
        | Some a -> f a

    member this.Return(x) =
        Some x

let maybe = new MaybeBuilder()

I gave the name MaybeBuilder instead of divideByBuilderbecause the optional result problem that we solve with a computational expression is quite common, and the word maybe – established name
for this thing.

Now that we have defined the steps for maybelet's rewrite the original code using them.

let divideByWorkflow init x y z =
    maybe
        {
        let! a = init |> divideBy x
        let! b = a |> divideBy y
        let! c = b |> divideBy z
        return c
        }

Looks much nicer! Expression maybe completely hidden the branches!

And, if we test the code, we will get the same result as before:

let good = divideByWorkflow 12 3 2 1
let bad = divideByWorkflow 12 3 0 1

Chain of checks in “else branches”

In the previous example with division, we only had to continue the calculations if the next step was successful.

But sometimes we need something different. Sometimes control flow depends on the sequence of checks in “else branches”. Check the first condition and if it is true, you are done. Otherwise, check the second, and if it is false, check the third, and so on.

Let's take a look at a simple example. Let's say we have three dictionaries and we want to find a value by key. Each check can succeed or fail, so we need to chain the checks together.

let map1 = [ ("1","One"); ("2","Two") ] |> Map.ofList
let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
let map3 = [ ("CA","California"); ("NY","New York") ] |> Map.ofList

let multiLookup key =
    match map1.TryFind key with
    | Some result1 -> Some result1   // нашли
    | None ->   // не нашли
        match map2.TryFind key with
        | Some result2 -> Some result2 // нашли
        | None ->   // не нашли
            match map3.TryFind key with
            | Some result3 -> Some result3  // нашли
            | None -> None // не нашли

Since everything is an expression in F#, we can't stop the calculation at any point, we have to put all the tests into one big expression.

Now let's see how this can be used:

multiLookup "A" |> printfn "Result for A is %A"
multiLookup "CA" |> printfn "Result for CA is %A"
multiLookup "X" |> printfn "Result for X is %A"

Works great, but is it possible to simplify our code?

Yes, definitely. Here's a builder for “else branches” that allows you to simplify these kinds of checks:

type OrElseBuilder() =
    member this.ReturnFrom(x) = x
    member this.Combine (a,b) =
        match a with
        | Some _ -> a  // получилось — используем a
        | None -> b    // не получилось — используем b
    member this.Delay(f) = f()

let orElse = new OrElseBuilder()

And this is how you can rewrite the check code using the builder.

let map1 = [ ("1","One"); ("2","Two") ] |> Map.ofList
let map2 = [ ("A","Alice"); ("B","Bob") ] |> Map.ofList
let map3 = [ ("CA","California"); ("NY","New York") ] |> Map.ofList

let multiLookup key = orElse {
    return! map1.TryFind key
    return! map2.TryFind key
    return! map3.TryFind key
    }

Again, let's make sure the code works as expected.

multiLookup "A" |> printfn "Result for A is %A"
multiLookup "CA" |> printfn "Result for CA is %A"
multiLookup "X" |> printfn "Result for X is %A"

Asynchronous calls with callback functions

Finally, let's take a look at callback functions. The standard way to make asynchronous calls in .NET is to use AsyncCallback delegatewhich is called when an asynchronous operation completes.

Here is an example of how you can download a web page using this technique:

open System.Net
let req1 = HttpWebRequest.Create("http://fsharp.org")
let req2 = HttpWebRequest.Create("http://google.com")
let req3 = HttpWebRequest.Create("http://bing.com")

req1.BeginGetResponse((fun r1 ->
    use resp1 = req1.EndGetResponse(r1)
    printfn "Downloaded %O" resp1.ResponseUri

    req2.BeginGetResponse((fun r2 ->
        use resp2 = req2.EndGetResponse(r2)
        printfn "Downloaded %O" resp2.ResponseUri

        req3.BeginGetResponse((fun r3 ->
            use resp3 = req3.EndGetResponse(r3)
            printfn "Downloaded %O" resp3.ResponseUri

            ),null) |> ignore
        ),null) |> ignore
    ),null) |> ignore

Lots of challenges BeginGetResponse And EndGetResponse, and nested lambda functions are quite difficult to understand. Important code (in our case these are print statements) is lost in the background of logic with callbacks.

In fact, large nesting of asynchronous code is a known problem, it even has its own name – “Pyramid of Fate” (although, in my opinion, None of the proposed solutions look elegant enough).

Naturally, we will never have to write such code in F#, because F# has a built-in evaluation expression asyncwhich simplifies the logic and eliminates nesting from the code (makes it flat).

open System.Net
let req1 = HttpWebRequest.Create("http://fsharp.org")
let req2 = HttpWebRequest.Create("http://google.com")
let req3 = HttpWebRequest.Create("http://bing.com")

async {
    use! resp1 = req1.AsyncGetResponse()
    printfn "Downloaded %O" resp1.ResponseUri

    use! resp2 = req2.AsyncGetResponse()
    printfn "Downloaded %O" resp2.ResponseUri

    use! resp3 = req3.AsyncGetResponse()
    printfn "Downloaded %O" resp3.ResponseUri

    } |> Async.RunSynchronously

Later in this series of articles we will understand how the process works async.

Conclusion

So, we got acquainted with several simple examples of computational expressions, both “before” and “after”. The examples do a good job of showing which problems computational expressions are suitable for solving.

  • In the logging example, we needed side effects to accompany each step of the calculation.
  • In the division example, we needed elegant error handling so that we could focus on the success scenario.
  • In the dictionary example, we wanted to abort a large computation as soon as we got a result.
  • And finally, in the asynchronous code example, we wanted to hide the callback functions and get rid of the pyramid of fate.

What all of these examples have in common is that the computational expressions do some work behind the scenes, between individual steps.

If you need a bad analogy, think of compute expressions as scripts that are executed after commits in SVN or git: or as triggers that are called after every database update. In fact, that's exactly what computational expressions are: a construct that allows you to silently run glue code in the background so you can focus on the front where the important code is running.

Why are they called “computational expressions”? Well, it is obvious that we are talking about a special type of expression, so at least with one word everything is clear. I believe the F# team originally wanted to call them “expressions-that-do-something-in-the-background-between-each-computation”, but then they somehow thought that was too cumbersome and decided give them the name “computational expressions” instead.

A little about the difference between a “computation expression” and a “process” (workflow). When I say “computational expression” I mean the syntax from {...} And let!. “Process” refers to specific implementations as we discuss them.
Not all implementations of computational expressions are processes. For example, we can talk about the “process async” or “process maybe“, but “the process seq” doesn't sound right.

In the example below I would say that maybe is the process we are using and the code is in curly braces { let! a = .... return c } — computational expression.

maybe
    {
    let! a = x |> divideBy y
    let! b = a |> divideBy w
    let! c = b |> divideBy z
    return c
    }

Now you might want to write your first computational expression, but first we need to understand continuations. This is the topic of the next article.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *