F# tuples
last modified May 17, 2025
Tuples in F# are lightweight data structures that group together multiple values, possibly of different types. They are immutable, ordered collections that provide a convenient way to return multiple values from functions or to temporarily group related values without defining a custom type.
Creating tuples
Tuples in F# are created by enclosing comma-separated values in parentheses. While tuples of any size can be defined, they are most commonly used with a manageable number of elements to maintain readability and efficiency. F# provides built-in optimizations for tuples containing up to seven elements, often referred to as "tuple pairs" through "tuple septuples," but larger tuples can still be created and used without restrictions.
// Pair (2-tuple) let nameAge = ("John", 32) printfn "Name and age: %A" nameAge // Triple (3-tuple) let rgbColor = (255, 128, 64) printfn "RGB color: %A" rgbColor // Mixed types let personData = ("Alice", 28, 165.5, true) printfn "Person data: %A" personData // Single-element tuples do not exist in F# // let single = ("hello",) // Empty tuple (unit) let empty = () printfn "Empty tuple: %A" empty
This example shows different ways to create tuples. Note that a single-element
tuples do not exist in F# and the empty tuple ()
represents the
unit
type.
λ dotnet fsi creating_tuples.fsx Name and age: ("John", 32) RGB color: (255, 128, 64) Person data: ("Alice", 28, 165.5, true) Empty tuple: ()
Omitting parentheses
F# allows tuples to be defined without parentheses, making the syntax cleaner
and more concise. However, this can lead to confusion when the syntax is
ambiguous. For example, the expression 1, 2 + 3
is interpreted as a
tuple (1, 5)
rather than the intended (1, 2) + 3
. To
avoid ambiguity, it is recommended to use parentheses when creating tuples,
especially in complex expressions.
let nameAge = "John", 32 printfn "Name and age: %A" nameAge let rgbColor = 255, 128, 64 printfn "RGB color: %A" rgbColor let personData = "Alice", 28, 165.5, true printfn "Person data: %A" personData
This example shows that parentheses can be omitted when creating tuples.
Accessing tuple elements
F# provides several ways to access tuple elements. You can use pattern matching,
the fst
and snd
functions for pairs, or direct
deconstruction.
let person = ("Robert", "Novak", 45) // Using fst and snd (for pairs only) let pair = ("John", "Doe") printfn "First: %s, Last: %s" (fst pair) (snd pair) // Pattern matching match person with | (firstName, lastName, age) -> printfn "%s %s is %d years old" firstName lastName age // Deconstructing directly let (fName, lName, years) = person printfn "Deconstructed: %s %s, age %d" fName lName years // Accessing elements of larger tuples let coords = (10.5, 20.3, 5.0, "home") let (x, y, z, label) = coords printfn "Coordinates: %s (%.1f, %.1f, %.1f)" label x y z
This code demonstrates various ways to access tuple elements. Pattern matching
is the most flexible approach, while fst
and snd
are
convenient for pairs. Deconstruction assigns elements directly to variables.
λ dotnet fsi accessing_elements.fsx First: John, Last: Doe Robert Novak is 45 years old Deconstructed: Robert Novak, age 45 Coordinates: home (10.5, 20.3, 5.0)
Using tuples with functions
Tuples are commonly used to return multiple values from functions or to pass multiple arguments to functions in a structured way. They provide a simple alternative to out parameters or custom types in many cases.
// Function returning a tuple let divide x y = let quotient = x / y let remainder = x % y (quotient, remainder) let result = divide 15 4 printfn "15 divided by 4: %A" result // Function taking a tuple parameter let printCoordinates (x, y, z) = printfn "Position: (%.1f, %.1f, %.1f)" x y z let position = (3.5, 2.0, 1.5) printCoordinates position // Curried vs tuple arguments let addCurried x y = x + y let addTupled (x, y) = x + y printfn "Curried add: %d" (addCurried 3 4) printfn "Tupled add: %d" (addTupled (3, 4))
This example shows functions returning tuples and accepting tuple parameters. Note the difference between curried functions (multiple parameters) and functions taking a single tuple parameter. The tuple version requires all arguments to be supplied at once.
λ dotnet fsi functions_with_tuples.fsx 15 divided by 4: (3, 3) Position: (3.5, 2.0, 1.5) Curried add: 7 Tupled add: 7
Tuple pattern matching
Pattern matching with tuples allows for concise branching logic based on the structure and contents of tuples. This is a powerful feature for handling different cases in your code.
let classifyPoint (x, y) = match (x, y) with | (0.0, 0.0) -> "Origin" | (_, 0.0) -> "On x-axis" | (0.0, _) -> "On y-axis" | (x, y) when x > 0.0 && y > 0.0 -> "Quadrant I" | (x, y) when x < 0.0 && y > 0.0 -> "Quadrant II" | (x, y) when x < 0.0 && y < 0.0 -> "Quadrant III" | _ -> "Quadrant IV" printfn "%s" (classifyPoint (0.0, 0.0)) printfn "%s" (classifyPoint (2.5, 0.0)) printfn "%s" (classifyPoint (-1.0, 3.0)) printfn "%s" (classifyPoint (3.0, -4.0)) let describeSize (width, height) = match (width, height) with | (w, h) when w = h -> "Square" | (w, h) when w > h -> "Landscape" | _ -> "Portrait" printfn "%s" (describeSize (100, 100)) printfn "%s" (describeSize (200, 100)) printfn "%s" (describeSize (80, 120))
The example demonstrates using tuple pattern matching to classify points in a
2D plane and to describe rectangle proportions. The underscore _
acts as a wildcard, matching any value.
λ dotnet fsi pattern_matching.fsx Origin On x-axis Quadrant II Quadrant IV Square Landscape Portrait
Tuple type annotations
Tuple types can be explicitly annotated to specify the types of their elements. This is useful for documentation and can help catch type errors at compile time.
// Annotating tuple types let person: string * int = ("Alice", 30) let coords: float * float * string = (45.2, -122.6, "Portland") // Function with annotated tuple parameter let printPerson (p: string * int) = let (name, age) = p printfn "%s is %d years old" name age printPerson person // Type inference with tuples let getDimensions () : int * int = let width = 800 let height = 600 (width, height) let (screenWidth, screenHeight) = getDimensions() printfn "Resolution: %dx%d" screenWidth screenHeight
This code shows how to add type annotations to tuples. The asterisk *
separates the types of tuple elements. Annotations can be added to tuple
variables, parameters, and return types.
λ dotnet fsi type_annotations.fsx Alice is 30 years old Resolution: 800x600
Comparing tuples
Tuples in F# support structural comparison when all their elements are comparable. Comparison is done element by element in order.
let point1 = (2, 3) let point2 = (2, 4) let point3 = (1, 5) printfn "point1 = point2? %b" (point1 = point2) printfn "point1 < point2? %b" (point1 < point2) printfn "point1 > point3? %b" (point1 > point3) // Sorting a list of tuples let points = [(1, 5); (2, 3); (2, 4); (1, 4)] let sorted = List.sort points printfn "Sorted points: %A" sorted // Comparing tuples with different types let a = ("apple", 3) let b = ("banana", 2) printfn "'apple' vs 'banana': %b" (a < b)
The example demonstrates tuple comparison. Tuples are compared by their first elements, then second elements if the first are equal, and so on. This makes them naturally sortable when all elements are comparable.
λ dotnet fsi comparing_tuples.fsx point1 = point2? false point1 < point2? true point1 > point3? true Sorted points: [(1, 4); (1, 5); (2, 3); (2, 4)] 'apple' vs 'banana': true
F# tuples are versatile, lightweight data structures that excel at temporarily grouping related values and returning multiple values from functions. Their simple syntax, pattern matching support, and built-in comparison make them ideal for many programming scenarios. While they shouldn't replace proper data types for complex domain modeling, tuples are an essential tool in any F# programmer's toolkit.