8. Tuples and destructuring
A tuple groups a fixed number of values that can have different types, written (a, b), exactly the
Python tuple. A record gives its fields names, while a tuple identifies them by position, which
suits a short pairing like a name and a score. Because the parentheses are also grouping, Pyfun
reads them by how many elements are inside: () is unit, the empty value from lesson 1, (x) is
just x in parentheses, and a real tuple has two or more elements. There is no one element tuple.
You take a tuple apart the same way you take an ADT apart, by matching. A single variable pattern
covers a tuple completely, so one case is exhaustive:
let swap p =
match p:
case (a, b): (b, a)
let names = ["ada", "alan"]
let scores = [10, 9]
let line pair =
match pair:
case (name, score): f"{name}: {score}"
let lines = List.zip names scores |> List.map line
let byName = List.zip names scores |> Map.ofList
let adaScore = Option.withDefault 0 (Map.tryFind "ada" byName)
print (swap (1, 2))
print lines
byName |> Map.toList |> print
print adaScore
(2, 1)
['ada: 10', 'alan: 9']
[('ada', 10), ('alan', 9)]
10
List.zip pairs two lists element by element into a list of tuples, so List.zip names scores is
[("ada", 10), ("alan", 9)]. Mapping line over that list destructures each pair in the case
and binds name and score, the way Python’s for name, score in pairs unpacks as it goes.
Tuples also bridge lists and maps. Map.ofList builds a Map from a list of pairs, and
Map.toList turns a map back into its pairs, so a zip followed by Map.ofList is a compact way to
build a lookup table from two parallel lists.
Lists destructure too
The same match works over a List. A list pattern names elements in brackets, just like the
literal that builds one: [] is the empty list, [only] matches a list of exactly one, and a
*rest star soaks up any number of elements. Python allows one star per pattern, and so does Pyfun,
but it can sit anywhere: [x, *rest] is head and tail, [*init, last] is everything but the last,
and [first, *mid, last] binds the two ends with the middle collected between them.
let describe xs =
match xs:
case []: "empty"
case [only]: f"just {only}"
case [first, *mid, last]: f"{first} ... {last}"
print (describe [])
print (describe [7])
print (describe [1, 2, 3, 4])
empty
just 7
1 ... 4
An empty-list arm plus a star arm covers every list, so a match built from [] and one starred
pattern is exhaustive with no case _. The [first, *mid, last] arm needs at least two elements,
which is why the [] and [only] arms come first to catch the shorter lists.
Exercise
List.zip has paired each name with its score, and the case (name, score) arm has already
destructured a pair for you. Fill the hole so each pair becomes a line like ada: 10, then the
program joins the lines with a comma. Use the bound name and score in an interpolated string.
let names = ["ada", "alan"]
let scores = [10, 9]
let line pair =
match pair:
case (name, score): ?
let lines = List.zip names scores |> List.map line
lines |> String.join ", " |> print
The checker reports:
note: hole `?` has type `'a`
--> 6:25
|
6 | case (name, score): ?
| ^
1 unfilled hole
Expected output:
ada: 10, alan: 9
Show solution
let names = ["ada", "alan"]
let scores = [10, 9]
let line pair =
match pair:
case (name, score): f"{name}: {score}"
let lines = List.zip names scores |> List.map line
lines |> String.join ", " |> print
The case (name, score) arm binds both parts of the pair, and the f-string builds one line per
pair before String.join stitches them together.