Basics 26 - Computation Expressions

What are Computation Expressions?

Computation Expressions (CE's) in F# are all about syntactic convenience. There are two different aspects:

  1. Using existing CEs: seq, query, async, task and lazy

  2. Building new/custom CEs, aka building CE builder types

When it comes to using existing CEs, life isn't too complicated. You study the documentation and use it.

However, when it comes to building CE builder types:

  1. This is one of the most complicated and difficult-to-understand areas in F#.

  2. Before you can start, you have to have a thorough understanding of many functional patterns/abstractions: monoids, monads and applicative functors.

  3. Sadly, the documentation available on building CE builder types is poor at best.

  4. When learning how to build CE builder types, you may find examples that are academic and shallow. Such examples cover only certain areas of CEs and can lead you to understand/use CEs in a very specific way. For instance, you'll often encounter option CE or validation CE. While these examples are not wrong, there is not a whole lot one can build on top of these.

  5. A CE builder type, in my view, is where two extreme ideas meet - functional abstractions and the abstraction (short-circuiting ability) that the F# compiler is offering.

At this point, my goal is to give you an overview of the existing CEs: seq, query, async, task and lazy. Later, I will discuss functional patterns, abstractions and CE builder types in a detailed series of articles.

Computation Expressions and Magic Keywords

The general form of a CE is:

expr
{
    // your code
}

With the CE, you will find usage of the following keywords

  1. let!

  2. and!

  3. do!

  4. yield

  5. yield!

  6. return

  7. return!

  8. match!

In isolation, these keywords are point-less. The usage of these keywords is dependent on what CE builder type is involved. When these keywords are used, the compiler emits the code that stitches what you have written with CE builder type functions. This will make sense after you have used some of the existing CEs.

Please note: not all keywords are available with all CEs.

Sequence Expressions - seq

The purpose of seq CE is to make writing sequences easier.

// A hard-coded sequence
let seq1 =
    seq {
        1
        2
        3
        4
        5
    }

// A sequence from 1 to 10
let seq2 = seq { 1..10 }

// A sequence from 1 to 10, skip 2
let seq3 = seq { 1..2..10 }

// A sequence based on loop
let seq4 = seq { for i in 1..10 -> i * i }

// A sequence with yield
// yield = here is an item for the sequence
let seq5 =
    seq {
        for i in 1..10 do
            yield i * i
    }

// A sequence with yield!
// yield! flattens the inner sequence,
// making it part of the outer sequence
// yield! = here are various items for the sequence
let seq6 =
    seq {
        yield!
            seq { 1; 2; 3; 4; 5 }

        yield!
            seq { 1; 2; 3; 4; 5 }

        yield!
            seq { 1; 2; 3; 4; 5 }
    }

let seq7 =
    seq {
        for _ in 1..10 do
            yield!
                seq {
                    1
                    2
                    3
                    4
                    5
                }
    }

Query Expressions - query

The purpose of query CE is to provide support for LINQ in F#. In other words, you can use SQL-like queries in F# code.

The query CE is full of functions for selecting, filtering, ordering, transforming etc. Check out https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/query-expressions for details.

type Digit = { InLetters: string; Value: int }

let digits =
    [ { InLetters = "Zero"; Value = 0 }
      { InLetters = "One"; Value = 1 }
      { InLetters = "Two"; Value = 2 }
      { InLetters = "Three"; Value = 3 }
      { InLetters = "Four"; Value = 4 }
      { InLetters = "Five"; Value = 5 }
      { InLetters = "Siz"; Value = 6 }
      { InLetters = "Seven"; Value = 7 }
      { InLetters = "Eight"; Value = 8 }
      { InLetters = "Nine"; Value = 9 } ]

// Gets all the items from digits
// q1 is digit seq
let q1 =
    query {
        for digit in digits do
            select digit
    }

// Gets all the items from digits
// where digit.value < 5
// Selects only the InLetter part
// q2 is string seq
let q2 =
    query {
        for digit in digits do
            where (digit.Value < 5)
            select digit.InLetters
    }

Async Expressions - async

The purpose of async CE is to provide support for asynchronous programming in F#. I will discuss async and task CEs later through detailed articles. This is just an overview.

open System.IO
open System.Text

let readAllText file =
    async {
        try
            let fInfo = FileInfo(file)

            use fs = File.OpenRead(file)

            let! bytes = fs.AsyncRead((int) fInfo.Length)

            return Encoding.UTF8.GetString(bytes)
        with ex ->
            return $"Operation failed: {ex}"
    }

Task Expressions - task

The purpose of task CE is to provide support for .NET Framework's Task-based asynchronous programming in F#. I will discuss async and task CEs later through detailed articles. This is just an overview.

open System.IO

let readAllText file =
    task {
        try
            let! content = File.ReadAllTextAsync(file)

            return content
        with ex ->
            return $"Operation failed: {ex}"
    }

Lazy Expressions - lazy

The lazy expressions are used for lazily evaluating results. Such expressions, as opposed to eager expressions, are evaluated only when needed. Also, such expressions are evaluated once.

// Printing a message each time add2 is called
// This is to check how many times add2 is called
let add2 num =
    printfn "add2 called for %d" num
    num + 2

// Checks condition
// If true, evaluates num, and prints
// If false, num is not evaluated
let printNumIfCondition condition (num: int Lazy) =
    if condition then
        printfn "true. num: %d" (num.Force())
    else
        printfn "false"

// Creatinga lazy expression which should be evaluated once
// lazyAdd2to10 is int lazy, same as
// lazyAdd2to10 is Lazy<int>
let lazyAdd2to10 = lazy (add2 10)

printNumIfCondition true lazyAdd2to10
printNumIfCondition true lazyAdd2to10
printNumIfCondition false lazyAdd2to10

// Result
// Expression is evaluated only once, despite printNumIfCondition being
// called twice with true
// add2 called for 10
// true. num: 12
// true. num: 12
// false

If you have reached so far, congratulations.

Keep reading!