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.