Lezione 1 di 6 · 16 min di lettura

Tuple

Due o tre valori, anche di tipo diverso, tenuti insieme al volo. Restituire più risultati da una funzione, e mettere in coppia gli elementi di due liste.

Valori diversi, insieme

Una lista tiene tanti valori dello stesso tipo. Ma a volte ti servono pochi valori di tipo diverso, tenuti insieme: un nome e un voto, una moneta e quante ne servono, un quoziente e un resto. Per questo ci sono le tuple.

src/tuples.gleam
import gleam/io

pub fn main() -> Nil {
  let pair = #(3, "three")
  let triple = #("Ada", 36, True)
  echo pair
  echo pair.0
  echo pair.1
  echo triple.2
  io.println("Done")
}
output
src/tuples.gleam:6
#(3, "three")
src/tuples.gleam:7
3
src/tuples.gleam:8
"three"
src/tuples.gleam:9
True
Done

Una tupla si scrive #( … ), con i valori separati da virgole: è la sintassi che il compilatore ti aveva suggerito nel modulo 1, quando provavi a raggruppare con le parentesi tonde. Il tipo di una tupla elenca i tipi dei suoi elementi, in ordine: pair è un #(Int, String), triple un #(String, Int, Bool). Una tupla ha sempre un numero fisso di elementi, deciso quando la scrivi.

Gli elementi si leggono con il punto e la posizione, contando da 0: pair.0 è il primo, pair.1 il secondo. Se chiedi una posizione che non esiste, pair.2, il programma non compila (Out of bounds tuple index): il compilatore sa quanti elementi ha ogni tupla.

Smontare una tupla con let

Più che con .0 e .1, di solito una tupla si smonta con un pattern, direttamente nel let:

gleam
  let #(number, word) = pair

Ora number vale 3 e word vale "three". È un pattern come quelli di case, ma usato in un let: si può fare perché questo pattern corrisponde sempre. Ogni #(Int, String) ha esattamente due elementi, non ci sono altre forme possibili. (Con una lista non si potrebbe: let [a, b] = lista non compila, perché la lista potrebbe avere tre elementi, o nessuno.)

Se un elemento non ti serve, _: let #(number, _) = pair.

Restituire più valori

L’uso più comune delle tuple: una funzione che deve restituire due risultati. Una funzione restituisce sempre un valore solo, ma quel valore può essere una tupla.

src/divmod.gleam
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let #(hours, minutes) = divmod(137, 60)
  io.println(int.to_string(hours) <> " h " <> int.to_string(minutes) <> " min")
}

/// The quotient and the remainder of an integer division.
fn divmod(a: Int, b: Int) -> #(Int, Int) {
  #(a / b, a % b)
}
output
2 h 17 min

Il tipo di ritorno #(Int, Int) dice chiaramente cosa arriva, e il let con il pattern dà subito un nome a ciascun pezzo.

Quiz

Con let #(a, b) = #(1, 2) e poi let #(a, b) = #(b, a), quanto valgono a e b?

Mettere in coppia due liste

Nella sfida del modulo 3 avevamo due liste da percorrere insieme, le monete e i loro conteggi. list.zip le “cuce” in una lista di coppie:

src/pairs.gleam
import gleam/int
import gleam/io
import gleam/list

pub fn main() -> Nil {
  let coins = [200, 100, 50]
  let counts = [3, 0, 1]
  let pairs = list.zip(coins, counts)
  echo pairs

  pairs
  |> list.filter(fn(pair) { pair.1 > 0 })
  |> list.each(fn(pair) {
    let #(coin, count) = pair
    io.println(int.to_string(count) <> " x " <> int.to_string(coin))
  })
}
output
src/pairs.gleam:9
[#(200, 3), #(100, 0), #(50, 1)]
3 x 200
1 x 50

Il primo elemento di coins con il primo di counts, il secondo con il secondo, e così via. Se una lista è più lunga, gli elementi in più vengono ignorati. Una lista di tuple è comodissima: ogni elemento porta con sé tutto quello che serve, e non c’è più il rischio di percorrere due liste “sfasate”. Il viaggio inverso lo fa list.unzip.

Le tuple si usano anche dentro i pattern. Una funzione ricorsiva su una lista di coppie può smontare tutto in un colpo:

gleam
fn lines(pairs: List(#(Int, Int))) -> List(String) {
  case pairs {
    [] -> []
    [#(_, 0), ..rest] -> lines(rest)
    [#(coin, count), ..rest] -> [
      int.to_string(count) <> " x " <> int.to_string(coin),
      ..lines(rest)
    ]
  }
}

[#(_, 0), ..rest] si legge “una lista che comincia con una coppia il cui secondo elemento è 0”.

Due accumulatori in uno

C’è un altro posto dove le tuple salvano la giornata: list.fold. L’accumulatore è un valore, ma se ti servono due cose, per esempio il minimo e il massimo, quel valore può essere una tupla:

gleam
  list.fold([4, 9, 2], #(100, 0), fn(acc, n) {
    #(int.min(acc.0, n), int.max(acc.1, n))
  })

vale #(2, 9): una sola passata sulla lista, due risultati.

Esercizio · sul tuo computer

Le temperature della settimana

Nel progetto exercises crea src/temperatures.gleam. Hai due liste:

gleam
  let days = ["Mon", "Tue", "Wed", "Thu"]
  let temps = [12, 15, 9, 14]
  1. Con list.zip e list.each, stampa una riga per giorno, come Mon: 12.
  2. Scrivi min_max(numbers: List(Int)) -> #(Int, Int) che restituisce il minimo e il massimo con un solo list.fold. Per una lista vuota restituisce #(0, 0); altrimenti l’accumulatore parte dal primo elemento, come minimo e come massimo.
  3. Stampa il risultato smontandolo con un let.
output
Mon: 12
Tue: 15
Wed: 9
Thu: 14
Min: 9, max: 15
Mostra una soluzione (prima prova da solo!)
src/temperatures.gleam
import gleam/int
import gleam/io
import gleam/list

pub fn main() -> Nil {
  let days = ["Mon", "Tue", "Wed", "Thu"]
  let temps = [12, 15, 9, 14]

  list.zip(days, temps)
  |> list.each(fn(pair) {
    let #(day, temp) = pair
    io.println(day <> ": " <> int.to_string(temp))
  })

  let #(low, high) = min_max(temps)
  io.println("Min: " <> int.to_string(low) <> ", max: " <> int.to_string(high))
}

fn min_max(numbers: List(Int)) -> #(Int, Int) {
  case numbers {
    [] -> #(0, 0)
    [first, ..rest] ->
      list.fold(rest, #(first, first), fn(acc, n) {
        let #(low, high) = acc
        #(int.min(low, n), int.max(high, n))
      })
  }
}

Partire dal primo elemento evita di dover scegliere un “minimo iniziale” arbitrario (come il 100 dell’esempio della lezione, che sbaglierebbe con temperature più alte di 100). Dentro la funzione del fold, smontare l’accumulatore con un let rende il codice più leggibile di acc.0 e acc.1.

Ricapitolando

  • Una tupla #(3, "three") tiene insieme un numero fisso di valori, anche di tipo diverso. Il tipo è #(Int, String).
  • tupla.0, tupla.1: gli elementi per posizione, da 0. Una posizione inesistente non compila.
  • let #(a, b) = tupla smonta una tupla: si può fare con let perché il pattern corrisponde sempre.
  • Una funzione restituisce più valori restituendo una tupla: fn divmod(a: Int, b: Int) -> #(Int, Int).
  • list.zip(a, b) crea una lista di coppie; list.unzip fa il contrario.
  • Le tuple vanno anche nei pattern ([#(_, 0), ..rest]) e negli accumulatori di fold.
  • Per gruppi di valori con un significato, meglio un tipo con i campi con un nome.

Nella prossima lezione costruiamo il primo tipo tutto nostro: un tipo che può avere solo alcuni valori, scelti da noi.