ZetCode

F# expressions

last modified May 17, 2025

In this article, we explore expressions in F#. Expressions are the fundamental building blocks of F# programs, representing computations that produce values.

An expression in F# is any piece of code that evaluates to a value. Unlike statements in imperative languages, F# treats all code constructs as expressions, meaning every operation results in a value. This includes simple literals, complex computations, function calls, and even control flow structures like conditionals.

Expressions play a fundamental role in F#, allowing developers to build logic in a modular and predictable way. Since expressions always return a value, code remains concise and avoids unnecessary side effects. For example, a conditional expression such as if x > 0 then "Positive" else "Negative" produces a string result, eliminating the need for explicit return statements.

All control flow structures in F# behave as expressions, including loops and pattern matching. A match expression can evaluate a value and produce an output based on predefined cases. For instance, match n with | 0 -> "Zero" | 1 -> "One" | _ -> "Other" returns a string based on the given input.

Expressions can be combined to form more complex expressions, supporting functional programming principles such as composition. Functions are expressions in F#, meaning they can be passed as arguments, returned as results, and applied in various ways to build dynamic logic.

Computation expressions offer additional capabilities, enabling structured control flows for tasks such as asynchronous programming, sequence manipulation, and query processing. These expressions simplify the management of side effects while preserving functional purity.

F# basic expressions

Simple expressions in F# include literals and basic operations.

basic.fsx
// Literal expressions
let number = 42
let text = "Hello"
let truth = true

// Arithmetic expressions
let sum = 5 + 3 * 2
let power = 2.0 ** 8.0

// String expressions
let greeting = text + ", F#!"
let interpolated = $"The answer is {number}"

printfn "%d" sum
printfn "%f" power
printfn "%s" greeting
printfn "%s" interpolated

We demonstrate various basic expressions in F#.

let sum = 5 + 3 * 2

Arithmetic expressions follow standard operator precedence rules.

λ dotnet fsi basic.fsx
11
256.000000
Hello, F#!
The answer is 42

F# conditional expressions

If/then/else constructs are expressions that return values.

conditional.fsx
let describeNumber n =
    if n % 2 = 0 then "even"
    else "odd"

let result = describeNumber 7
printfn "7 is %s" result

// Ternary-style expression
let max a b = if a > b then a else b
printfn "Max of 5 and 3 is %d" (max 5 3)

// Nested conditionals
let rating score =
    if score >= 90 then "A"
    elif score >= 80 then "B"
    elif score >= 70 then "C"
    else "F"

printfn "Score 85 gets: %s" (rating 85)

The example shows how if/then/else constructs are expressions in F#.

if n % 2 = 0 then "even"
else "odd"

The entire if expression evaluates to either "even" or "odd".

λ dotnet fsi conditional.fsx
7 is odd
Max of 5 and 3 is 5
Score 85 gets: B

F# block expressions

Expressions can be grouped into blocks with multiple statements.

blocks.fsx
let calculateTotal price quantity =
    // This is a block expression
    let subtotal = price * quantity
    let tax = subtotal * 0.08m
    subtotal + tax  // Last expression is the return value

let total = calculateTotal 25.0m 3m
printfn "Total: %M" total

// Another block expression example
let message =
    let name = "Alice"
    let age = 30
    $"{name} is {age} years old"

printfn "%s" message

The example demonstrates block expressions with multiple let bindings.

let subtotal = price * quantity
let tax = subtotal * 0.08m
subtotal + tax

The block evaluates to the last expression (subtotal + tax).

λ dotnet fsi blocks.fsx
Total: 81.000000
Alice is 30 years old

F# function expressions

Functions are first-class expressions in F#. They can be defined using function expressions or lambda expressions. Functions can be passed as arguments to other functions, returned from functions, and stored in data structures.

functions.fsx
// Function as an expression
let square x = x * x

// Lambda expression
let cube = fun x -> x * x * x

// Higher-order function
let applyTwice f x = f (f x)

let result1 = applyTwice square 2
let result2 = applyTwice cube 2

printfn "Square twice: %d" result1
printfn "Cube twice: %d" result2

// Function composition
let negate x = -x
let squareThenNegate = negate >> square
printfn "Composed: %d" (squareThenNegate 3)

In the example we use various function expressions in F#. The applyTwice function takes a function and an argument, applies the function twice to the argument, and returns the result. The negate function negates its argument. The squareThenNegate function composes the negate and square functions using the function composition operator (>>).

let cube = fun x -> x * x * x

Lambda expressions are anonymous functions that can be assigned to names.

λ dotnet fsi functions.fsx
Square twice: 16
Cube twice: 256
Composed: -9

F# match expressions

Pattern matching with match expressions is powerful in F#. It allows you to deconstruct data types and match against specific patterns. The match expression is similar to switch statements in other languages but is more expressive.

match.fsx
let describeNumber n =
    match n with
    | 0 -> "Zero"
    | 1 -> "One"
    | x when x < 0 -> "Negative"
    | x when x % 2 = 0 -> "Even"
    | _ -> "Odd"

printfn "0: %s" (describeNumber 0)
printfn "42: %s" (describeNumber 42)
printfn "-5: %s" (describeNumber -5)
printfn "7: %s" (describeNumber 7)

// Matching on tuples
let pointCategory (x, y) =
    match x, y with
    | 0, 0 -> "Origin"
    | _, 0 -> "X-axis"
    | 0, _ -> "Y-axis"
    | _ -> "Other"

printfn "(0,5): %s" (pointCategory (0, 5))

The example demonstrates matching on integers and tuples. The match expression uses guards (when) to add additional conditions to the patterns. The underscore pattern (_) is a wildcard that matches anything not matched by previous patterns.

match n with
| 0 -> "Zero"
| 1 -> "One"

Match expressions evaluate to the expression in the matching branch.

λ dotnet fsi match.fsx
0: Zero
42: Even
-5: Negative
7: Odd
(0,5): Y-axis

F# sequence expressions

Sequence expressions generate sequences lazily. They are defined using the seq keyword and can include loops and conditionals. They are useful for generating infinite sequences or sequences that depend on external data sources. Sequences are evaluated on demand, meaning that values are generated only when needed. This allows for efficient memory usage and can lead to improved performance in certain scenarios.

sequences.fsx
// Basic sequence expression
let numbers = seq { 1..10 }

// Sequence with filtering
let evens = seq {
    for n in 1..20 do
        if n % 2 = 0 then
            yield n
}

// Sequence with transformation
let squares = seq {
    for n in 1..5 do
        yield n * n
}

printfn "Numbers: %A" (numbers |> Seq.take 3)
printfn "Evens: %A" (evens |> Seq.take 3)
printfn "Squares: %A" (squares |> Seq.toList)

// More complex sequence
let coordinates = seq {
    for x in 1..3 do
        for y in 1..3 do
            yield (x, y)
}

printfn "Coordinates: %A" (coordinates |> Seq.toList)

The example shows how to create sequences using the seq expression. The seq expression allows you to define a sequence of values using a for loop and yield keyword. The yield keyword is used to produce values in the sequence. The example demonstrates a basic sequence, a filtered sequence, and a transformed sequence.

seq {
    for n in 1..20 do
        if n % 2 = 0 then
            yield n
}

Sequence expressions generate values on demand (lazily).

λ dotnet fsi sequences.fsx
Numbers: seq [1; 2; 3]
Evens: seq [2; 4; 6]
Squares: [1; 4; 9; 16; 25]
Coordinates: [(1, 1); (1, 2); (1, 3); (2, 1); (2, 2); (2, 3); (3, 1); (3, 2); (3, 3)]

F# computation expressions

Computation expressions provide syntactic sugar for monadic operations. They allow you to define custom workflows that can be used to encapsulate asynchronous or stateful computations. Computation expressions are defined using the let! and do! keywords, which allow you to bind values and perform side effects within the expression.

computations.fsx
open System.Net.Http

// Using the task computation expression (F# 6+)
let fetchData (url: string) = task {
    let client = new HttpClient()
    let! response = client.GetAsync(url)
    let! content = response.Content.ReadAsStringAsync()
    return content
}

let fetchWebcode = fetchData "https://webcode.me"

printfn "fetchWebcode result: %s" fetchWebcode.Result

In the example, we use the task computation expression to fetch data from a URL. It allows us to write asynchronous code in a more readable way. The let! keyword is used to bind the result of an asynchronous operation to a variable. The computation expression is executed when the function is called. The result is a task that can be awaited or executed synchronously.

In this article we've explored the fundamental role of expressions in F#. Understanding expressions is key to writing idiomatic F# code, as everything in F# is an expression that evaluates to a value.

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.