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.
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")
}src/tuples.gleam:6
#(3, "three")
src/tuples.gleam:7
3
src/tuples.gleam:8
"three"
src/tuples.gleam:9
True
DoneUna 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:
let #(number, word) = pairOra 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.
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)
}2 h 17 minIl 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:
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))
})
}src/pairs.gleam:9
[#(200, 3), #(100, 0), #(50, 1)]
3 x 200
1 x 50Il 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:
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:
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:
let days = ["Mon", "Tue", "Wed", "Thu"]
let temps = [12, 15, 9, 14]- Con
list.zipelist.each, stampa una riga per giorno, comeMon: 12. - Scrivi
min_max(numbers: List(Int)) -> #(Int, Int)che restituisce il minimo e il massimo con un sololist.fold. Per una lista vuota restituisce#(0, 0); altrimenti l’accumulatore parte dal primo elemento, come minimo e come massimo. - Stampa il risultato smontandolo con un
let.
Mon: 12
Tue: 15
Wed: 9
Thu: 14
Min: 9, max: 15Mostra una soluzione (prima prova da solo!)
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) = tuplasmonta una tupla: si può fare conletperché 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.unzipfa il contrario.- Le tuple vanno anche nei pattern (
[#(_, 0), ..rest]) e negli accumulatori difold. - 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.