Lezione 6 di 6 · 22 min di lettura
Riepilogo e sfida finale
Gli errori come valori in una pagina, tre domande di controllo, e il robot del modulo 4 che legge i comandi da un testo.
Guarda quanta strada
In questo modulo hai imparato che in Gleam le cose possono andare storte senza che niente “esploda”. Adesso sai:
- leggere e restituire
Result, con tipi di errore che dicono esattamente cosa è successo; - concatenare operazioni che possono fallire con
result.map,result.trye compagnia, e gestire liste di risultati; - scrivere quelle catene dritte con
use, e uscire in anticipo conbool.guard; - distinguere i problemi da gestire da quelli impossibili, e usare
todo,panic,let asserteassertquando serve davvero; - scrivere test con
gleam test, e leggere un test che fallisce.
La pagina da tenere accanto
| Cosa | Come si scrive |
|---|---|
| Un risultato | Result(Int, MyError): Ok(valore) o Error(errore) |
| Testo in numero | int.parse("42") → Ok(42); string.trim prima |
| Aprire un Result | case r { Ok(x) -> ... Error(e) -> ... } |
| Trasformare il valore | result.map(r, fn(x) { ... }) |
| Un altro passo che può fallire | result.try(r, fn(x) { ... }) |
| Valore o ripiego | result.unwrap(r, default) |
| Cambiare l’errore | result.replace_error(r, MyError), result.map_error |
| Liste | list.try_map (tutto o niente), list.filter_map (solo i successi), result.all |
| Catene dritte | use x <- result.try(operazione) |
| Uscita anticipata | use <- bool.guard(when: cond, return: valore) |
| Codice da scrivere | todo as "..." |
| Situazione impossibile | panic as "...", let assert Ok(x) = ... |
| Test | pub fn nome_test() { assert f(1) == 2 } in test/, poi gleam test |
Trovi tutto anche nel Codex (tasto K).
Quiz di controllo
Quiz
Quanto vale int.parse("10") |> result.map(fn(n) { n * 2 }) |> result.unwrap(0)?
Quiz
In una funzione, dopo use n <- result.try(int.parse(text)), cosa succede se text è "abc"?
Quiz
Un test pub fn total_test() { assert total([1, 2]) == 4 } fallisce con left: 3 e right: 4. Cosa significa?
La sfida: il robot che legge
Nel modulo 4 i comandi del robot erano una lista scritta nel codice. Ora arrivano da un testo, come quelli che una persona potrebbe digitare: "F3 R F2 L F1". F seguito da un numero è “avanti di tante caselle”, L e R sono le svolte. Il testo può contenere errori, e il robot deve dire con precisione quale.
Esercizio · sul tuo computer
Il robot che legge
Nel progetto exercises crea src/robot_text.gleam. Parti dai tipi Direction, Robot e Command e dalle funzioni run, turn_left, turn_right, describe della sfida del modulo 4 (copiali pure), e aggiungi un tipo di errore:
pub type CommandError {
UnknownCommand(String)
BadSteps(String)
}Poi scrivi:
parse_command(word: String) -> Result(Command, CommandError):"L"e"R"sono le svolte; una parola che comincia con"F"è unForward, se il resto è un numero maggiore di zero, altrimentiBadStepscon la parola intera; tutto il resto èUnknownCommand. (Ricordi il pattern"F" <> stepsdella lezione 2.5?)parse_program(text: String) -> Result(List(Command), CommandError): divide il testo sugli spazi, scarta i pezzi vuoti, e converte tutti i comandi conlist.try_map.run_program(text: String) -> Result(Robot, CommandError): conuseeresult.try, legge il programma e, se è valido, lo esegue conlist.foldpartendo da(0, 0)verso nord.report(result: Result(Robot, CommandError)) -> String, per stampare il risultato.
In main esegui i programmi "F3 R F2 L F1", "F2 X F1", "F R" e "F0":
F3 R F2 L F1 -> (2, 4) facing North
F2 X F1 -> unknown command: X
F R -> bad number of steps: F
F0 -> bad number of steps: F0Mostra una soluzione (prima prova da solo!)
import gleam/int
import gleam/io
import gleam/list
import gleam/result
import gleam/string
pub type Direction {
North
East
South
West
}
pub type Robot {
Robot(x: Int, y: Int, facing: Direction)
}
pub type Command {
Forward(steps: Int)
TurnLeft
TurnRight
}
pub type CommandError {
UnknownCommand(String)
BadSteps(String)
}
pub fn main() -> Nil {
["F3 R F2 L F1", "F2 X F1", "F R", "F0"]
|> list.each(fn(program) {
io.println(program <> " -> " <> report(run_program(program)))
})
}
pub fn run_program(text: String) -> Result(Robot, CommandError) {
use commands <- result.try(parse_program(text))
let start = Robot(x: 0, y: 0, facing: North)
Ok(list.fold(commands, start, run))
}
pub fn parse_program(text: String) -> Result(List(Command), CommandError) {
text
|> string.split(on: " ")
|> list.filter(fn(word) { word != "" })
|> list.try_map(parse_command)
}
pub fn parse_command(word: String) -> Result(Command, CommandError) {
case word {
"L" -> Ok(TurnLeft)
"R" -> Ok(TurnRight)
"F" <> steps ->
case int.parse(steps) {
Ok(n) if n > 0 -> Ok(Forward(n))
_ -> Error(BadSteps(word))
}
_ -> Error(UnknownCommand(word))
}
}
fn run(robot: Robot, command: Command) -> Robot {
case command {
TurnLeft -> Robot(..robot, facing: turn_left(robot.facing))
TurnRight -> Robot(..robot, facing: turn_right(robot.facing))
Forward(steps:) ->
case robot.facing {
North -> Robot(..robot, y: robot.y + steps)
South -> Robot(..robot, y: robot.y - steps)
East -> Robot(..robot, x: robot.x + steps)
West -> Robot(..robot, x: robot.x - steps)
}
}
}
fn turn_right(direction: Direction) -> Direction {
case direction {
North -> East
East -> South
South -> West
West -> North
}
}
fn turn_left(direction: Direction) -> Direction {
case direction {
North -> West
West -> South
South -> East
East -> North
}
}
fn report(result: Result(Robot, CommandError)) -> String {
case result {
Ok(robot) -> describe(robot)
Error(UnknownCommand(word)) -> "unknown command: " <> word
Error(BadSteps(word)) -> "bad number of steps: " <> word
}
}
fn describe(robot: Robot) -> String {
"("
<> int.to_string(robot.x)
<> ", "
<> int.to_string(robot.y)
<> ") facing "
<> direction_name(robot.facing)
}
fn direction_name(direction: Direction) -> String {
case direction {
North -> "North"
East -> "East"
South -> "South"
West -> "West"
}
}Qualche dettaglio da notare:
- Il programma è diviso in due fasi nette: prima si legge tutto il testo (dove possono esserci errori), poi si esegue (dove non ce ne sono più).
runelist.foldnon sanno nulla degli errori: lavorano su comandi già validi. list.try_mapsi ferma al primo comando sbagliato: in"F2 X F1"il robot non si muove nemmeno, perché il programma è rifiutato prima di partire.- In
parse_commandil pattern"F" <> stepssepara la lettera dal numero, e la guardiaOk(n) if n > 0scarta sia i numeri mancanti ("F") sia lo zero. - Tutte le funzioni che leggono sono
pub: potresti scrivere dei test perparse_commandintest/robot_text_test.gleam. Prova:assert parse_command("F3") == Ok(Forward(3))eassert parse_command("Q") == Error(UnknownCommand("Q"))(conimport robot_text.{Forward, UnknownCommand, parse_command}in cima al test).
Cosa succede nel Modulo 6
Le liste sono perfette per mettere le cose in fila, ma per cercare sono lente: per trovare l’età di una persona nella rubrica della lezione 4.5 bisognava scorrerla tutta. Nel prossimo modulo arrivano i dizionari (Dict), che associano chiavi e valori e trovano una chiave in un attimo, e gli insiemi (Set). Vedremo anche i tipi opachi, che permettono a un modulo di nascondere come sono fatti i suoi dati e di proteggerli da un uso sbagliato.