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