Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Open in the playground

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.