ZetCode

F# do bindings

last modified May 3, 2025

In this article, we explore do bindings in F#. The do keyword is used to execute code for its side effects when a return value is not needed.

A do binding executes expressions for their side effects rather than their return values. Unlike let bindings, do doesn't bind a name to a value. Do bindings are commonly used for initialization code, main program execution, and other operations where the action is more important than the result.

F# Basic do Binding

In F#, a do binding is used to execute a single expression for its side effects. Unlike let bindings, which capture return values, do bindings simply execute a statement without storing the result. This makes them ideal for actions such as printing output, performing logging, or calling functions where the result does not need to be preserved.

basic.fsx
do printfn "Hello, there!"

let x = 5
do printfn "The value of x is %d" x

In this example, the first do binding calls printfn to print "Hello, there!" without capturing a return value. The second do binding prints the value of x using printfn. Since do bindings are designed for executing statements rather than expressions, they are commonly used for simple, standalone operations.

λ dotnet fsi basic.fsx
Hello, World!
The value of x is 5

F# do Binding in Modules

In F#, do bindings within a module execute when the module is first accessed. These bindings are useful for performing initialization tasks, such as setting up global resources or running setup code before any module functions or values are used. Unlike functions that must be explicitly called, do bindings ensure that initialization logic runs automatically upon module access.

modules.fsx
module Startup =
    do printfn "Initializing module..."

    let calculate x = x * 2

    do printfn "Module initialization complete"

printfn "Before accessing module"
let result = Startup.calculate 10
printfn "Result: %d" result

In this example, the first do binding prints "Initializing module..." when the module is accessed for the first time. The second do binding further prints "Module initialization complete", confirming that initialization is complete. This ensures that any setup logic inside the module executes only once. Finally, the function calculate is called, and the result is printed. This demonstrates how do bindings help structure module initialization in F#.

λ dotnet fsi modules.fsx
Before accessing module
Initializing module...
Module initialization complete
Result: 20

F# do binding in types

In F#, do bindings within a type are executed whenever a new instance of the type is created. These bindings are often used for initialization tasks, such as setting up resources or performing side-effect operations when an object is instantiated. Unlike traditional constructors, do bindings allow for concise and direct execution of statements without explicitly defining an initialization method.

types.fsx
type Person(name: string) =
    do printfn "Creating person: %s" name

    member this.Greet() =
        printfn "Hello, my name is %s" name

let p = Person("John Doe")
p.Greet()

In this example, the do binding inside the Person type executes when a new instance of Person is created. This prints the message "Creating person: John Doe" as a side effect. After instantiation, the Greet method can be called, which outputs another message. Using do bindings in class types is a convenient way to ensure initialization logic runs automatically upon object creation.

λ dotnet fsi types.fsx
Creating person: John Doe
Hello, my name is John Doe

F# do binding with sequences

Do bindings can execute multiple statements in sequence.

sequence.fsx
do
    printfn "Starting program..."
    printfn "Loading configuration..."
    printfn "Initializing services..."
    printfn "Ready to begin processing"

let calculate x = x * 2

Shows a do binding with multiple statements executed in order.

do
    printfn "Starting program..."
    printfn "Loading configuration..."

The do keyword followed by indented block executes multiple statements.

λ dotnet fsi sequence.fsx
Starting program...
Loading configuration...
Initializing services...
Ready to begin processing

F# do Binding vs let Binding

The let and do bindings serve different purposes in F#. The let binding is used to associate a name with a value, capturing the result of an expression. In contrast, the do binding is primarily used for executing expressions with side effects, such as printing to the console, without storing the return value.

comparison.fsx
// The let binding captures the return value
let message = printfn "This is a let binding"

// The do binding executes a statement but discards the return value
do printfn "This is a do binding"

printfn "message value: %A" message

In this example, printfn returns the unit value (). The let binding stores this value in the message variable, making it available for further use. Conversely, the do binding simply executes the statement, discarding the return value.

λ dotnet fsi comparison.fsx
This is a let binding
This is a do binding
message value: ()

F# do binding in scripts

Do bindings are commonly used in F# scripts for top-level code.

scripts.fsx
#r "nuget: Newtonsoft.Json"

do printfn "Loading JSON library..."

open Newtonsoft.Json

let data = """{"name":"John","age":30}"""
do printfn "Parsing JSON data..."

let person = JsonConvert.DeserializeObject<{| name: string; age: int |}>(data)
do printfn "Name: %s, Age: %d" person.name person.age

Shows typical use of do bindings in an F# script file.

do printfn "Loading JSON library..."

Marks important steps in script execution without needing return values.

λ dotnet fsi scripts.fsx
Loading JSON library...
Parsing JSON data...
Name: John, Age: 30

F# do binding with async

Do bindings are essential for working with async computations.

async.fsx
open System
open System.Threading.Tasks

let fetchData() = async {
    do! Task.Delay(1000) |> Async.AwaitTask
    return "Data loaded"
}

do
    printfn "Starting async operation..."
    async {
        let! data = fetchData()
        printfn "%s" data
    } |> Async.Start

Console.ReadLine() |> ignore

Demonstrates do! in async workflows and top-level do bindings.

do! Task.Delay(1000) |> Async.AwaitTask

The do! keyword is used in async workflows for side-effecting operations.

λ dotnet fsi async.fsx
Starting async operation...
Data loaded

F# do binding limitations

The do binding in F# is designed for executing side-effect expressions, such as printing to the console or performing asynchronous operations. However, it comes with specific constraints: it cannot be used as part of an expression, and it does not return a value that can be assigned to a variable. These limitations ensure that do bindings are used correctly within F#'s functional paradigm.

limitations.fsx
// Valid do binding
do printfn "This works"

// Invalid - can't use do in expressions
// let x = do printfn "This won't work"

// Valid - do binding in computation expression
async {
    do printfn "Inside async"
    do! Async.Sleep(1000)
}

// Valid - multiple statements
do
    printfn "First"
    printfn "Second"

In this example, the first do binding executes printfn without returning a value. Attempting to assign do printfn to a variable results in an error because do cannot be part of an expression. However, do is valid within computation expressions, such as in async { ... }, where it helps manage side effects within asynchronous workflows. Additionally, multiple statements can be grouped under a single do binding, ensuring clean and structured execution.

λ dotnet fsi limitations.fsx
This works
Inside async
First
Second

In this article we've explored do bindings in F# and their role in handling side effects and imperative operations in a primarily functional language.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.