F# query expressions
last modified May 17, 2025
In this article, we delve into query expressions in F#—a powerful feature that enables writing queries in a declarative and intuitive manner.
F# query expressions offer an elegant way to retrieve and transform data from various sources, including collections, databases, and structured datasets. They provide a SQL-like syntax that integrates seamlessly with F#'s functional programming capabilities, allowing developers to build efficient and expressive queries. By leveraging query expressions, F# enables concise data manipulation while maintaining readability and composability in code.
Basic query expressions
The simplest query expressions select data from a source collection. F# provides
several operators like select, where, and
sortBy to filter and transform data.
open System
let vals = [| 1; 2; 3; 4; 5; 6|]
let lst = query {
for e in vals do
last
}
Console.WriteLine(lst)
let fst = query {
for e in vals do
head
}
Console.WriteLine(fst)
let n = query {
for e in vals do
nth 3
}
Console.WriteLine(n)
This example demonstrates basic query operations. The last operator
gets the last element, head gets the first element, and
nth gets the element at a specific index. Query expressions are
enclosed in query { } blocks.
λ dotnet fsi basic_query.fsx 6 1 4
Filtering with where
The where operator filters elements based on a condition. It's
similar to the WHERE clause in SQL or the filter operation in functional
programming.
open System
let vals = [| 1; 2; 3; 4; 5; 6|]
let res = query {
for v in vals do
where (v <> 3)
select v
}
for e in res do
Console.WriteLine(e)
Console.WriteLine(vals.GetType())
This code filters out the value 3 from the array. The where
clause contains the condition, and select specifies what to
return. The result is an IEnumerable that we can iterate over.
λ dotnet fsi where_operator.fsx 1 2 4 5 6 Microsoft.FSharp.Core.FSharpOption`1[System.Int32[]]
Counting and selecting elements
Query expressions can count elements and perform more complex operations on
custom types. The count operator returns the number of elements
that match the query.
open System
type User = {
Name: string
Occupation: string
}
let users = [
{ Name = "John Doe"; Occupation = "gardener" }
{ Name = "Roger Roe"; Occupation = "driver" }
{ Name = "Thomas Monroe"; Occupation = "trader" }
{ Name = "Gregory Smith"; Occupation = "teacher" }
{ Name = "Lucia Bellington"; Occupation = "teacher" }
]
let n = query {
for user in users do
select user
count
}
Console.WriteLine(n)
let last = query {
for user in users do
last
}
Console.WriteLine(last)
Console.WriteLine("teachers:")
let teachers = query {
for user in users do
where (user.Occupation = "teacher")
select user
}
teachers |> Seq.iter Console.WriteLine
This example works with a list of User records. We count all users, get the
last user, and filter users by occupation. The where clause
filters for teachers, and select returns the matching users.
λ dotnet fsi count_operator.fsx
5
{ Name = "Lucia Bellington"; Occupation = "teacher" }
teachers:
{ Name = "Gregory Smith"; Occupation = "teacher" }
{ Name = "Lucia Bellington"; Occupation = "teacher" }
Sorting data
The sortBy and thenBy operators allow sorting data by
one or more fields. sortBy performs the primary sort, while
thenBy adds secondary sorting criteria.
open System
type User = {
FirstName: string
LastName: string
Salary: int
}
let users = [
{ FirstName = "Robert"; LastName = "Novak"; Salary = 1770 }
{ FirstName = "John"; LastName = "Doe"; Salary = 1230 }
{ FirstName = "Lucy"; LastName = "Novak"; Salary = 670 }
{ FirstName = "Ben"; LastName = "Walter"; Salary = 2050 }
{ FirstName = "Robin"; LastName = "Brown"; Salary = 2300 }
{ FirstName = "Amy"; LastName = "Doe"; Salary = 1250 }
{ FirstName = "Joe"; LastName = "Draker"; Salary = 1190 }
{ FirstName = "Janet"; LastName = "Doe"; Salary = 980 }
{ FirstName = "Peter"; LastName = "Novak"; Salary = 990 }
{ FirstName = "Albert"; LastName = "Novak"; Salary = 1930 }
]
let sorted = query {
for user in users do
sortBy user.LastName
thenBy user.Salary
select user
}
sorted |> Seq.iter Console.WriteLine
This code sorts users first by last name, then by salary. The result is a sequence of users ordered alphabetically by last name, with users having the same last name ordered by salary.
λ dotnet fsi sorting.fsx
{ FirstName = "Robin"; LastName = "Brown"; Salary = 2300 }
{ FirstName = "John"; LastName = "Doe"; Salary = 1230 }
{ FirstName = "Amy"; LastName = "Doe"; Salary = 1250 }
{ FirstName = "Janet"; LastName = "Doe"; Salary = 980 }
{ FirstName = "Joe"; LastName = "Draker"; Salary = 1190 }
{ FirstName = "Lucy"; LastName = "Novak"; Salary = 670 }
{ FirstName = "Peter"; LastName = "Novak"; Salary = 990 }
{ FirstName = "Robert"; LastName = "Novak"; Salary = 1770 }
{ FirstName = "Albert"; LastName = "Novak"; Salary = 1930 }
{ FirstName = "Ben"; LastName = "Walter"; Salary = 2050 }
Grouping and aggregation
Query expressions support grouping data and performing aggregations like sum,
average, count, etc. The groupBy operator groups elements by a
key, and aggregate functions can be applied to each group.
open System.Linq
type Revenue =
{ Id: int
Quarter: string
Amount: int }
let revenues = [
{ Id = 1; Quarter = "Q1"; Amount = 2340 };
{ Id = 2; Quarter = "Q1"; Amount = 1200 };
{ Id = 3; Quarter = "Q1"; Amount = 980 };
{ Id = 4; Quarter = "Q2"; Amount = 340 };
{ Id = 5; Quarter = "Q2"; Amount = 780 };
{ Id = 6; Quarter = "Q3"; Amount = 2010 };
{ Id = 7; Quarter = "Q3"; Amount = 3370 };
{ Id = 8; Quarter = "Q4"; Amount = 540 }
]
query {
for revenue in revenues do
groupBy revenue.Quarter into g
where (g.Count() = 2)
select {| Quarter = g.Key
Total = g.Sum(fun c -> c.Amount) |}
}
|> Seq.iter (fun e -> printfn "%A" e)
This example groups revenues by quarter, filters for quarters with exactly 2 entries, and calculates the total amount for each qualifying quarter. The result is an anonymous record with the quarter and total amount.
λ dotnet fsi grouping.fsx
{ Quarter = "Q1"; Total = 4520 }
{ Quarter = "Q2"; Total = 1120 }
{ Quarter = "Q3"; Total = 5380 }
F# query expressions provide a powerful, declarative way to work with data. They offer SQL-like syntax for filtering, sorting, grouping, and transforming data while maintaining F#'s type safety and functional programming benefits. Whether working with in-memory collections or external data sources, query expressions can make your data processing code more readable and maintainable.