F# list comprehensions
last modified May 17, 2025
In this article, we explore list comprehensions in F# - a concise and powerful way to generate lists by transforming and filtering existing collections.
A list comprehension in F# is a syntactic construct for creating lists
based on existing lists or sequences. Using the yield
keyword within
list expressions, we can generate new lists by specifying the elements to include,
often with filtering conditions and transformations. List comprehensions provide
a declarative way to work with collections that is both readable and expressive.
Basic list comprehensions
List comprehensions are a powerful feature in F# that allow you to create lists from existing lists or sequences. They provide a concise syntax for generating new lists by applying transformations and filtering conditions.
// Simple range comprehension let numbers = [1..5] printfn "Range: %A" numbers // Explicit list comprehension let colors = ["red"; "green"; "blue"] printfn "Colors: %A" colors // Comprehension with yield let squares = [for x in 1..5 do yield x * x] printfn "Squares: %A" squares // Inline computation let cubes = [for x in 1..3 -> x * x * x] printfn "Cubes: %A" cubes
The code demonstrates basic list comprehensions in F#. The first line creates a list of numbers from 1 to 5 using the range operator. The second line creates a list of colors. The third line uses a list comprehension to generate squares of the numbers from 1 to 5. The fourth line uses an inline computation to create a list of cubes.
λ dotnet fsi basic.fsx Range: [1; 2; 3; 4; 5] Colors: ["red"; "green"; "blue"] Squares: [1; 4; 9; 16; 25] Cubes: [1; 8; 27]
Filtering conditions
List comprehensions can also include filtering conditions to select specific elements from the source list. This allows you to create new lists that meet certain criteria.
// Filter even numbers let evens = [for x in 1..10 do if x % 2 = 0 then yield x] printfn "Evens: %A" evens // Multiple conditions let filtered = [ for x in 1..20 do if x % 2 = 0 && x % 3 = 0 then yield x ] printfn "Divisible by 2 and 3: %A" filtered // Using when guard let primes = [ for x in 2..20 do if [for d in 2..x/2 do if x % d = 0 then yield d] = [] then yield x ] printfn "Primes: %A" primes
The code demonstrates filtering in list comprehensions. In the first comprehension, we filter even numbers from 1 to 10. In the second comprehension, we filter numbers that are divisible by both 2 and 3. The third comprehension uses a guard clause to filter prime numbers from 2 to 20.
λ dotnet fsi filtering.fsx Evens: [2; 4; 6; 8; 10] Divisible by 2 and 3: [6; 12; 18] Primes: [2; 3; 5; 7; 11; 13; 17; 19]
Nested list comprehensions
Nested list comprehensions allow you to create lists of lists or perform Cartesian products. They can also be used to flatten nested lists.
// Cartesian product let product = [ for x in 1..3 do for y in 1..3 do yield (x, y) ] printfn "Cartesian product: %A" product // Nested with filtering let pythagoreanTriples = [ for a in 1..10 do for b in a..10 do for c in b..10 do if a*a + b*b = c*c then yield (a, b, c) ] printfn "Pythagorean triples: %A" pythagoreanTriples // Flattening nested lists let matrix = [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]] let flattened = [for row in matrix do for num in row do yield num] printfn "Flattened matrix: %A" flattened
The example shows how to use multiple generators in list comprehensions. The first comprehension creates a Cartesian product of two ranges. The second comprehension generates Pythagorean triples by checking the condition a2 + b2 = c2. The third comprehension flattens a nested list (matrix) into a single list.
λ dotnet fsi nested.fsx Cartesian product: [(1, 1); (1, 2); (1, 3); (2, 1); (2, 2); (2, 3); (3, 1); (3, 2); (3, 3)] Pythagorean triples: [(3, 4, 5); (6, 8, 10)] Flattened matrix: [1; 2; 3; 4; 5; 6; 7; 8; 9]
F# list comprehension vs sequence expression
List comprehensions can be compared with sequence expressions in F#. While list comprehensions create concrete lists immediately, sequence expressions generate sequences lazily. This means that the elements of a sequence are computed on demand, which can lead to performance benefits when working with large datasets.
#time "on" // List comprehension (eager evaluation) let listSquares = [for x in 1L..1000000L -> x * x] printfn "List length: %d" (List.length listSquares) let listSum = [for x in 1L..1000000L -> x * x] |> List.sum printfn "List sum: %d" listSum #time "off" #time "on" // Sequence expression (lazy evaluation) let seqSquares = seq {for x in 1L..1000000L -> x * x} printfn "Sequence length: %d" (seqSquares |> Seq.length) let seqSum = seq {for x in 1L..1000000L -> x * x} |> Seq.sum printfn "Sequence sum: %d" seqSum #time "off"
The example demonstrates the differences between list and sequence
comprehensions. The first part creates a list of squares using a list
comprehension and calculates the sum of the squares. The second part creates a
sequence of squares using a sequence expression and calculates the sum of the
squares. The #time
directive is used to measure the time taken
for each operation. The list comprehension eagerly evaluates the entire list,
while the sequence expression lazily evaluates the elements as needed.
This can lead to performance differences, especially for large datasets.
λ dotnet fsi comparison.fsx List length: 1000000 List sum: 333333833333500000 Real: 00:00:00.084, CPU: 00:00:00.125, GC gen0: 1, gen1: 0, gen2: 0 Sequence length: 1000000 Sequence sum: 333333833333500000 Real: 00:00:00.098, CPU: 00:00:00.093, GC gen0: 0, gen1: 0, gen2: 0
Using pattern matching
Pattern matching is a powerful feature in F# that allows you to destructure and analyze data types. It can be used within list comprehensions to filter and transform elements based on their structure.
type Shape = | Circle of radius: float | Rectangle of width: float * height: float | Triangle of _base: float * height: float let shapes = [ Circle 5.0 Rectangle (4.0, 6.0) Triangle (3.0, 4.0) Circle 2.5 Rectangle (5.0, 5.0) ] // Filter and extract using pattern matching let circles = [ for shape in shapes do match shape with | Circle r -> yield r | _ -> () ] printfn "Circle radii: %A" circles // Transform with pattern matching let areas = [ for shape in shapes do match shape with | Circle r -> yield System.Math.PI * r * r | Rectangle (w, h) -> yield w * h | Triangle (b, h) -> yield 0.5 * b * h ] printfn "Areas: %A" areas
The code defines a discriminated union type Shape
with three
constructors: Circle
, Rectangle
, and
Triangle
. It creates a list of shapes and demonstrates how to use
pattern matching within list comprehensions. The first comprehension filters and
extracts the radii of circles, while the second comprehension calculates the
areas of different shapes using pattern matching.
λ dotnet fsi pattern_matching.fsx Circle radii: [5.0; 2.5] Areas: [78.53981634; 24.0; 6.0; 19.63495408; 25.0]
Using lambdas
You can use lambda expressions inside list comprehensions to define custom filtering or transformation logic inline. This example shows how to use a lambda to filter positive numbers from a list.
let vals = [ 1; -2; -3; 4; 5 ] [ for v in vals do let f = fun e -> e > 0 if f(v) then yield v ] |> printfn "%A"
Here, a lambda fun e -> e > 0
is defined inside the
comprehension to check if a value is positive. Only positive values are yielded
to the resulting list.
λ dotnet fsi lambda_in_comprehension.fsx [1; 4; 5]
The yield keyword
The yield
keyword is used in list comprehensions to add a single
element to the resulting list. Each yield
statement produces one
item in the output list.
let chars = [ 'a' .. 'z' ] let res2 = [for e in chars do yield [e; e; e] ] printfn "%A" res2 let words = [ "sky"; "cloud"; "park"; "rock"; "war" ] let res = [ for e in words do yield e ] printfn "%A" res
In the first example, yield
adds a list of three repeated
characters for each character in the alphabet, resulting in a list of lists. In
the second example, yield
adds each word as a single element to the
resulting list.
λ dotnet fsi yield_example.fsx [[a; a; a]; [b; b; b]; ...; [z; z; z]] ["sky"; "cloud"; "park"; "rock"; "war"]
The yield! keyword
The yield!
keyword is used to add all elements of a collection to
the resulting list. It flattens the collection, inserting each element
individually into the output list.
let res = [ for a in 1..5 do yield! [ a .. a + 3 ] ] printfn "%A" res let words = [ "sky"; "cloud"; "park"; "rock"; "war" ] let res2 = [ for e in words do yield! e ] printfn "%A" res2
In the first example, yield!
inserts all elements from the sublist
[a .. a + 3]
for each a
in the range, resulting in a
flattened list of numbers. In the second example, yield!
inserts
each character from every word into the resulting list, producing a list of all
characters from all words.
λ dotnet fsi yield_bang_example.fsx [1; 2; 3; 4; 2; 3; 4; 5; 3; 4; 5; 6; 4; 5; 6; 7; 5; 6; 7; 8] ['s'; 'k'; 'y'; 'c'; 'l'; 'o'; 'u'; 'd'; 'p'; 'a'; 'r'; 'k'; 'r'; 'o'; 'c'; 'k'; 'w'; 'a'; 'r']
In this article, we've explored the power and flexibility of list comprehensions in F#. They provide a concise, declarative way to create and transform lists that is both readable and expressive.