Lezione 2 di 6 · 25 min di lettura
Dati senza tipo
Quando i dati arrivano da fuori, il compilatore non sa cosa contengono. Il tipo Dynamic, i decoder di gleam/dynamic/decode, i record decodificati campo per campo, e gli errori che dicono dove guardare.
Non fidarsi, controllare
Nella lezione precedente il compilatore si fidava della firma di una funzione esterna, e una firma sbagliata faceva schiantare il programma. Il problema è più generale: ogni volta che un dato arriva da fuori, da una funzione Erlang, da un file, da una risposta di un server, il compilatore non può sapere com’è fatto. Può essere quello che ti aspetti, o no.
Gleam ha un modo sicuro per gestire questi dati: invece di fingere di conoscerne il tipo, li dichiari di tipo Dynamic, “qualsiasi cosa”. Poi, prima di usarli, li controlli con un decoder, che restituisce un Result: Ok con un valore Gleam ben tipato, oppure Error con la descrizione di cosa non andava. Da lì in poi, sei di nuovo nel mondo sicuro dei tipi.
Il tipo Dynamic
Dynamic sta nel modulo gleam/dynamic della libreria standard. Un valore Dynamic può essere un intero, una stringa, una lista, un dizionario: Gleam non lo sa, e non ti lascia usarlo direttamente. Non puoi sommarlo, né stamparlo con io.println.
Per fare esperimenti, il modulo ha delle funzioni che trasformano un valore normale in Dynamic: dynamic.int(42), dynamic.string("Ada"), dynamic.list([...]), e dynamic.properties, che costruisce un dizionario. Nei programmi veri i Dynamic arrivano da fuori; qui ce li fabbrichiamo da soli.
I primi decoder
I decoder stanno nel modulo gleam/dynamic/decode. Per ogni tipo di base ce n’è uno già pronto: decode.int, decode.float, decode.string, decode.bool. E decode.run applica un decoder a un dato:
import gleam/dynamic
import gleam/dynamic/decode
import gleam/io
import gleam/string
pub fn main() -> Nil {
let data = dynamic.int(42)
io.println(string.inspect(decode.run(data, decode.int)))
io.println(string.inspect(decode.run(data, decode.string)))
io.println(dynamic.classify(data))
}Ok(42)
Error([DecodeError("String", "Int", [])])
IntIl primo run chiede “è un intero?”, e la risposta è Ok(42): un Int vero, da usare come vuoi. Il secondo chiede “è una stringa?”, e riceve un errore. Un errore di decodifica è un record DecodeError con tre campi: expected (cosa si aspettava, "String"), found (cosa ha trovato, "Int"), e path, il percorso dentro il dato, che qui è vuoto. decode.run restituisce una lista di errori, come vedremo tra poco.
dynamic.classify dice che tipo di dato c’è dentro un Dynamic: comodo per i messaggi di errore.
Per le liste c’è decode.list, che riceve il decoder degli elementi: decode.list(decode.string) decodifica una List(String), e fallisce se anche un solo elemento non è una stringa.
Decodificare un record
I dati che arrivano da fuori sono quasi sempre strutturati: un utente con un nome, un’età, una lista di linguaggi. Per trasformarli in un record Gleam si costruisce un decoder campo per campo, con use e decode.field:
pub type Person {
Person(name: String, age: Int, languages: List(String))
}
fn person_decoder() -> decode.Decoder(Person) {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)
use languages <- decode.field("languages", decode.list(decode.string))
decode.success(Person(name:, age:, languages:))
}Si legge quasi come una frase: “prendi il campo name e decodificalo come stringa; prendi il campo age come intero; prendi languages come lista di stringhe; se tutto va bene, il risultato è un Person“. decode.success è il decoder che riesce sempre, con il valore che gli dai: l’ultimo passo, quello che mette insieme i pezzi.
Il use è quello della lezione 5.3: ogni riga passa il resto della funzione a decode.field come callback. Il risultato di person_decoder() non è un Person: è un decoder di Person, di tipo decode.Decoder(Person), una ricetta pronta da applicare a qualsiasi dato con decode.run.
Dove sbaglia, e quante volte
Proviamo il decoder su un dato giusto e su due sbagliati:
import gleam/dynamic
import gleam/dynamic/decode
import gleam/io
import gleam/string
pub type Person {
Person(name: String, age: Int, languages: List(String))
}
fn person_decoder() -> decode.Decoder(Person) {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)
use languages <- decode.field("languages", decode.list(decode.string))
decode.success(Person(name:, age:, languages:))
}
pub fn main() -> Nil {
let ada =
dynamic.properties([
#(dynamic.string("name"), dynamic.string("Ada")),
#(dynamic.string("age"), dynamic.int(36)),
#(
dynamic.string("languages"),
dynamic.list([dynamic.string("Gleam"), dynamic.string("Erlang")]),
),
])
let joe =
dynamic.properties([
#(dynamic.string("name"), dynamic.string("Joe")),
#(dynamic.string("age"), dynamic.string("forty")),
#(dynamic.string("languages"), dynamic.list([dynamic.int(1)])),
])
let nobody = dynamic.properties([])
io.println(string.inspect(decode.run(ada, person_decoder())))
io.println(string.inspect(decode.run(joe, person_decoder())))
io.println(string.inspect(decode.run(nobody, person_decoder())))
}Ok(Person("Ada", 36, ["Gleam", "Erlang"]))
Error([DecodeError("Int", "String", ["age"]), DecodeError("String", "Int", ["languages", "0"])])
Error([DecodeError("Field", "Nothing", ["name"]), DecodeError("Field", "Nothing", ["age"]), DecodeError("Field", "Nothing", ["languages"])])Tre cose da notare:
- Il decoder non si ferma al primo errore: nei dati di Joe segnala sia l’età sia la lista. Se stai correggendo un file, è molto meglio sapere subito tutto quello che non va.
- Il
pathdice dove guardare:["age"]è il campoage;["languages", "0"]è il primo elemento (indice 0) della listalanguages. - Un campo che manca del tutto dà
expected: "Field"efound: "Nothing".
Quiz
Cosa restituisce decode.run(dynamic.string("7"), decode.int)?
Campi facoltativi
Non sempre un campo c’è. Se è facoltativo, decode.optional_field riceve anche un valore di riserva, da usare quando il campo manca:
use email <- decode.optional_field("email", "", decode.string)Se email c’è, dev’essere una stringa (altrimenti è un errore); se non c’è, il valore è "". Se invece vuoi distinguere “assente” da “presente”, usa Option: decode.optional_field("email", None, decode.optional(decode.string)).
Dal mondo esterno
Adesso mettiamo insieme le due lezioni. Ecco una funzione Erlang che restituisce un profilo, come farebbe una libreria che legge un file di configurazione scritto a mano da qualcuno, quindi senza garanzie sul contenuto: un map di Erlang, cioè un dizionario, con chiavi e valori binari (le stringhe di Gleam, che in Erlang si scrivono <<"name">>):
-module(profile_ffi).
-export([load/0]).
load() ->
#{<<"name">> => <<"Ada">>,
<<"age">> => 36,
<<"languages">> => [<<"Gleam">>, <<"Erlang">>]}.La funzione esterna, questa volta, non finge di conoscere la forma dei dati: restituisce Dynamic. E prima di usare il profilo, lo decodifichiamo:
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/io
import gleam/string
pub type Profile {
Profile(name: String, age: Int, languages: List(String), email: String)
}
@external(erlang, "profile_ffi", "load")
fn load_profile() -> Dynamic
fn profile_decoder() -> decode.Decoder(Profile) {
use name <- decode.field("name", decode.string)
use age <- decode.field("age", decode.int)
use languages <- decode.field("languages", decode.list(decode.string))
use email <- decode.optional_field("email", "", decode.string)
decode.success(Profile(name:, age:, languages:, email:))
}
pub fn main() -> Nil {
case decode.run(load_profile(), profile_decoder()) {
Ok(profile) ->
io.println(
profile.name <> " knows " <> string.join(profile.languages, ", "),
)
Error(errors) -> io.println("Invalid profile: " <> string.inspect(errors))
}
}Ada knows Gleam, ErlangSe un giorno il profilo cambiasse forma, il programma non si schianterebbe in un punto a caso: finirebbe nel ramo Error, con un messaggio che dice esattamente quale campo non va.
Esercizio · sul tuo computer
Il catalogo della biblioteca
Crea src/library.gleam, con un tipo Book(title: String, year: Int) e un book_decoder().
Costruisci con dynamic.properties due dati: il primo con title "Dune" e year 1965 (un intero), il secondo con lo stesso titolo ma year scritto come stringa, "1965". Decodificali entrambi, e stampa il risultato con una funzione describe:
- se va bene, titolo e anno separati da una virgola;
- se no, il primo errore della lista, nella forma
Error at <percorso>: expected <atteso>, found <trovato>, con i pezzi del percorso uniti da un punto.
Dune, 1965
Error at year: expected Int, found StringSuggerimento: un pattern come Error([decode.DecodeError(expected:, found:, path:), ..]) prende il primo errore e ne estrae i campi.
Mostra una soluzione (prima prova da solo!)
import gleam/dynamic
import gleam/dynamic/decode
import gleam/int
import gleam/io
import gleam/string
pub type Book {
Book(title: String, year: Int)
}
fn book_decoder() -> decode.Decoder(Book) {
use title <- decode.field("title", decode.string)
use year <- decode.field("year", decode.int)
decode.success(Book(title:, year:))
}
pub fn main() -> Nil {
let good =
dynamic.properties([
#(dynamic.string("title"), dynamic.string("Dune")),
#(dynamic.string("year"), dynamic.int(1965)),
])
let bad =
dynamic.properties([
#(dynamic.string("title"), dynamic.string("Dune")),
#(dynamic.string("year"), dynamic.string("1965")),
])
io.println(describe(decode.run(good, book_decoder())))
io.println(describe(decode.run(bad, book_decoder())))
}
fn describe(result: Result(Book, List(decode.DecodeError))) -> String {
case result {
Ok(book) -> book.title <> ", " <> int.to_string(book.year)
Error([decode.DecodeError(expected:, found:, path:), ..]) ->
"Error at "
<> string.join(path, ".")
<> ": expected "
<> expected
<> ", found "
<> found
Error([]) -> "Unknown error"
}
}Il ramo Error([]) non capiterà mai (un decoder che fallisce dà sempre almeno un errore), ma il compilatore non lo sa, e vuole che tutti i casi siano coperti.
Ricapitolando
- I dati che arrivano da fuori hanno tipo
Dynamic: Gleam non ti lascia usarli finché non li controlli. - Un decoder (
gleam/dynamic/decode) controlla unDynamice restituisceOk(valore)oError(lista di DecodeError); si applica condecode.run(dato, decoder). - Decoder pronti:
decode.int,decode.float,decode.string,decode.bool,decode.list(inner). - Un record si decodifica campo per campo:
use x <- decode.field("x", decoder), e alla finedecode.success(Record(...)). - Il decoder raccoglie tutti gli errori;
pathdice dove guardare. decode.optional_field(chiave, riserva, decoder)per i campi che possono mancare.Dynamicsolo per dati di forma davvero sconosciuta, seguito subito da un decoder; per il resto, tipi precisi.
Nella prossima lezione, la fonte di dati esterni più comune di tutte: il JSON.