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.
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.
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.
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.
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.
// 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.
#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.
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.
// 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.