Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Functional programming: Code with F# computation expressions

Learn when and how to use F# computation expressions to simplify your code.

Mar 21, 2024 • 5 Minute Read

Please set an alt value for this image...
  • Software Development

One of my favorite features of the F# language is computation expressions

Microsoft explains them as, “A convenient syntax for writing computations that can be sequenced and combined using control flow constructs and bindings. . . . Unlike other languages (such as do-notation in Haskell), they are not tied to a single abstraction, and do not rely on macros or other forms of metaprogramming to accomplish a convenient and context-sensitive syntax.”

In layman's terms, they're a really cool abstraction that allows a lot of super powerful transformations and operations in code flow. In this post, I explore functional programming in F# and share useful computation expressions you can use to streamline your code.

Table of contents

What is a computation expression in F#?

At its core, a computation expression is a type with members that takes values of some type and transforms those values in some way. There are all sorts of fancy functional programming terms that describe what's going on (you'll see the term monad thrown around a lot), but you don't really need to know all that to use and get value from computation expressions.

Why use F# computation expressions?

There are many reasons to use computation expressions, but one of the main benefits is readability. In functional programming, it’s very common to use Discriminated Unions and record types. With these kinds of types, you’ll often perform mapping operations to transform values from one type to another (e.g. SomeType<T> -> SomeType<U>). Computation expressions allow us to perform common operations like this in a way that can really improve the readability of those operations.

Under the hood, all that a computation expression does is call functions using a special syntax. In fact, most computation expressions will have a module exposing the implementation functions that can be called directly for when that may make more sense.

But the computation expression syntax allows us to write some code a little more declaratively, and sometimes that’s quite beneficial. For example, the following functions using the FsToolkit.ErrorHandling library are fully equivalent:

      // Without computation expression
let addResult: Result<int, string> =
  tryParseInt "35"
  |> Result.bind(fun x -> 
        tryParseInt “5”
        |> Result.bind(fun y -> 
               tryParseInt "2"
               |> Result.bind(fun z ->
                       add x y z
                   )
            )
      )

// With computation expression
let addResult: Result<int, string> = result {
  let! x = tryParseInt "35"
  let! y = tryParseInt "5"
  let! z = tryParseInt "2"
  return add x y z
}
    

Built-in F# computation expressions

First, let’s explore some built-in F# computation expressions that you may already be using without even knowing.

The seq computation expression

Probably the simplest example in F# is the built-in seq computation expression, which is exactly equivalent to the IEnumerable<T> interface in C#. 

seq isn't actually a keyword in F#. It's just a built-in computation expression that defines the members necessary to create a sequence of values. It allows you to write code like this:

      // yield statements are technically unnecessary here, but I'm including them for clarity in the comparison
let mySequence(): int seq =
  seq {
  	yield 1
  	yield 2
  	yield 3
  }
    

The equivalent C# code would be:

      IEnumerable<int> MySequence()
{
	yield return 1;
	yield return 2;
	yield return 3;
}
    

Async and task

Other useful built-in computation expressions include async and task. That's right, unlike in C# where you rely on compiler magic to translate async to a state machine, in F# it's just computation expressions. (Well, the implementation of task does rely on a little compiler magic, but that's just an optimization.)

Writing a custom computation expression: optional

One great example of a very simple custom computation expression is optional. What we're wanting is a cleaner way to deal with 't option values. If we have several things that may or may not be present, and we can only do a full operation if they're all there, we can use optional to make that code a lot cleaner.

Before we begin: Understanding the Option<’T> type

For some brief background, F# has a built-in type called Option<'T>. The type definition looks like this:

      type Option<'T> =
| Some of 'T
| None
    

This is commonly used to model situations where a value may or may not be present, similar to how null is used in many other languages (but F# tries to avoid the billion dollar mistake).

Thanks to the way the F# language works, we can use pattern matching on optional values to easily and cleanly handle things differently whether a value is present or not:

      let doSomething (maybeValue: string option) =
    match maybeValue with
    | Some value -> printfn $"Value was {value}"
    | None -> printfn "No value provided"
    

That's quite nice! We guarantee that a value is present and in the same operation retrieve the value. If the value isn’t there, we have a separate branch that handles that scenario.

Things may begin to get a little clunkier though, when multiple optional values are present, and we need to check for all of them to be present before proceeding. For example, say we have a piece of code like this:

      let doSomething (a: string option) (b: int option) (c: float option) =
	match a with
	| Some a' ->
    	    	match b with
    	    	| Some b' ->
        	    	match c with
        	    	| Some c' ->
            	    	    	doSomethingWithAll a' b' c' |> Some
        	    	| None -> None
    	    	| None -> None
	| None -> None
    

There's a lot of unwrapping going on here. It's very explicit what is going on here, but it's also pretty clunky to read through. In this example, we really are only able to proceed if all of the optional values are Some(x), otherwise we just want to return None.

It would be nice if we could optimize our code for the "happy path" and just handle the None case at the end. That's where the optional computation expression can come in handy.

Step 1: Create the computation expression builder

First we need to define a builder class:

      type OptionalBuilder() =
	member _.Bind(x : 'a option, f: 'a -> 'b option) = Option.bind f x
	member _.Return(x: 'a) = Some x
    

This class has only two members: Bind and Return. Bind is the function that will unwrap an option to it's inner value if it's Some, and if it's None it will just return None. Return essentially just wraps a value in Some.

The member names used for computation expressions have a well-defined pattern, but due to the nature of higher-kinded types, and the fact that any computation expression may not need all available functionality, they can't currently be modeled by a simple interface.

So a builder class doesn't need to implement an interface, it just has to define the relevant member methods. You can see which methods are available for this purpose with first-class support.

Step 2: Create an instance

Once we have our builder class, we create an instance of it, and that instance becomes the "keyword" to use in our computation expression:

      let optional = OptionalBuilder()
    

Step 3: Rewrite with the optional computation expression

Now we can use our optional computation expression to rewrite our code:

      let doSomething (a: string option) (b: int option) (c: float option) =
	optional {
    	    	let! a' = a
    	    	let! b' = b
    	    	let! c' = c
    	    	return doSomethingWithAll a' b' c'
	}
    

Much cleaner! We can now focus on the happy path, and if any of the options are None, we'll just return None from the whole function.

Wrapping up: Functional programming with F#

Out of the box, F# computation expressions can be used to support standard functional programming operations that you'd expect from monadic types. There are a lot more things you can do with the built-in computation expression functions, and you can also utilize custom operations to essentially create your own Domain-Specific Languages (DSLs)! 

Computation expressions are incredibly powerful, and I've used them when creating a simple SQL query builder, http request builder, and dependency injection container registrations. Try and see what you can build with them!

Josh DeGraw

Josh D.

Josh loves solving problems and building solutions. He dedicates himself to figuring out how to make things better and more efficient. He also enjoys working on front-end design systems/component libraries and developer tools. He has specialized in Typescript/React and C#/F# but is interested in other technologies as well. When he has time, he likes to contribute to open source projects (e.g. Fantomas) and enjoys collaborating on tools to improve the developer experience.

More about this author