Lezione 5 di 6 · 16 min di lettura
Tipi generici
Tipi con una variabile di tipo dentro, come List(a). Un tipo per i valori che potrebbero mancare, Option dalla libreria standard, e gli alias di tipo.
Il primo elemento, se c’è
Nella lezione 3.2 abbiamo scritto first_or: il primo elemento di una lista, oppure un valore di riserva. Il valore di riserva però è una scelta di chi chiama, e a volte non ce n’è uno sensato. Qual è il “primo elemento di riserva” di una lista di persone?
Sarebbe meglio che la funzione rispondesse onestamente: “ecco il primo elemento”, oppure “non c’è”. Due possibilità, una con un dato e una senza: è esattamente un tipo con varianti.
pub type MaybeInt {
JustInt(Int)
NothingInt
}Funziona per le liste di Int. Ma per le liste di String servirebbe MaybeString, poi MaybePerson… Lo stesso problema della funzione twice della lezione 2.3, e la stessa soluzione: una variabile di tipo.
Un tipo con un parametro
import gleam/io
pub type Maybe(a) {
Just(a)
Nothing
}
pub fn main() -> Nil {
echo first([3, 4])
echo first(["a", "b"])
echo first([])
echo with_default(first([]), 0)
io.println("Done")
}
fn first(items: List(a)) -> Maybe(a) {
case items {
[] -> Nothing
[x, ..] -> Just(x)
}
}
fn with_default(maybe: Maybe(a), default: a) -> a {
case maybe {
Just(value) -> value
Nothing -> default
}
}src/optional.gleam:9
Just(3)
src/optional.gleam:10
Just("a")
src/optional.gleam:11
Nothing
src/optional.gleam:12
0
DoneMaybe(a) è un tipo generico: a sta per un tipo qualsiasi, deciso da come lo usi. first([3, 4]) ha tipo Maybe(Int), first(["a", "b"]) ha tipo Maybe(String). La variante Just(a) contiene un valore di quel tipo; Nothing non contiene niente.
Nota anche che il campo di Just non ha un’etichetta: si può fare, e con un campo solo e ovvio si fa spesso. Il valore si prende con un pattern, Just(value).
(Attenzione al nome del file: il modulo si chiama optional e non maybe, perché maybe è una parola riservata di Erlang, e un modulo con quel nome fa fallire l’avvio del programma.)
Perché non restituire un valore speciale?
In molti linguaggi, quando qualcosa manca si restituisce un valore “speciale”: null, None, undefined, oppure -1 per un indice che non esiste. Il problema è che quel valore ha lo stesso tipo di quelli buoni, o si infila ovunque: niente ti obbliga a controllarlo, e prima o poi qualcuno lo usa come se fosse un valore vero.
Con Maybe(Int) questo non può succedere. Un Maybe(Int) non è un Int: non puoi sommarlo, non puoi passarlo a int.to_string. Per arrivare al numero devi passare da un case (o da una funzione come with_default), e il case ti obbliga a dire cosa fare quando il numero non c’è.
Option, dalla libreria standard
Un tipo per “c’è o non c’è” è così utile che la libreria standard ce l’ha già: si chiama Option(a), nel modulo gleam/option, con le varianti Some(a) e None. È identico al nostro Maybe, solo con altri nomi.
Per usare Some e None senza scrivere ogni volta option.Some, si importano per nome:
import gleam/option.{type Option, None, Some}Le graffe dopo il nome del modulo elencano le cose da importare direttamente: il tipo Option (preceduto da type) e le varianti None e Some. Il resto del modulo resta disponibile con il prefisso: option.unwrap(maybe, 0) è il nostro with_default, e option.map(Some(3), fn(x) { x * 2 }) applica una funzione al valore, se c’è, e vale Some(6).
Quiz
Con import gleam/option.{type Option, None, Some}, quale di queste righe non compila?
Alias di tipo
Un’ultima cosa sui tipi, piccola ma comoda. Nel corso abbiamo usato spesso Int per i centesimi: fn format_cents(cents: Int) -> String. Con un alias puoi dare un nome più parlante a un tipo esistente:
pub type Cents =
Int(Il formattatore lo scrive su due righe, così.) Ora fn format_cents(cents: Cents) -> String si legge meglio. Ma attenzione: un alias è solo un altro nome, non un tipo nuovo. Cents e Int sono intercambiabili, e il compilatore non ti impedirà di passare un’età dove servono dei centesimi. Per un tipo davvero separato serve un tipo personalizzato, come Cents(Int), con la sua variante.
Esercizio · sul tuo computer
La rubrica
Nel progetto exercises crea src/phonebook.gleam, con il tipo Person(name: String, age: Int) e la lista:
let people = [Person("Ada", 36), Person("Grace", 85)]Scrivi:
find_age(people: List(Person), name: String) -> Option(Int), ricorsiva: l’età della prima persona con quel nome, se c’è (usa una guardia sul nome);describe_age(people: List(Person), name: String) -> String, che usafind_agee uncasesuSomeeNone.
In main stampa describe_age per "Grace" e per "Linus":
Grace is 85
Linus: not foundMostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/option.{type Option, None, Some}
pub type Person {
Person(name: String, age: Int)
}
pub fn main() -> Nil {
let people = [Person("Ada", 36), Person("Grace", 85)]
io.println(describe_age(people, "Grace"))
io.println(describe_age(people, "Linus"))
}
fn find_age(people: List(Person), name: String) -> Option(Int) {
case people {
[] -> None
[person, ..] if person.name == name -> Some(person.age)
[_, ..rest] -> find_age(rest, name)
}
}
fn describe_age(people: List(Person), name: String) -> String {
case find_age(people, name) {
Some(age) -> name <> " is " <> int.to_string(age)
None -> name <> ": not found"
}
}Nella guardia person.name == name c’è un accessore: si può, perché leggere un campo non è una chiamata di funzione. E find_age è in coda: quando il primo non è la persona cercata, l’ultima cosa che fa è richiamarsi sul resto.
Ricapitolando
- Un tipo può avere un parametro:
pub type Maybe(a) { Just(a) Nothing }.Maybe(Int),Maybe(String)… sono tutti casi dello stesso tipo generico. List(a)e le tuple sono tipi generici già pronti.- In Gleam non esiste
null: un valore che può mancare ha un tipo che lo dice, e va aperto con uncase. - La libreria standard ha
Option(a), conSome(a)eNone, nel modulogleam/option. import gleam/option.{type Option, None, Some}importa tipo e varianti per nome.option.unwrapeoption.maplavorano sugliOptionsenzacase.pub type Cents = Intè un alias: un nome in più, non un tipo nuovo.
Nella prossima lezione, la sfida del modulo: un piccolo robot fatto di tipi, varianti e record.