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.try e compagnia, e gestire liste di risultati;
  • scrivere quelle catene dritte con use, e uscire in anticipo con bool.guard;
  • distinguere i problemi da gestire da quelli impossibili, e usare todo, panic, let assert e assert quando serve davvero;
  • scrivere test con gleam test, e leggere un test che fallisce.

La pagina da tenere accanto

CosaCome si scrive
Un risultatoResult(Int, MyError): Ok(valore) o Error(errore)
Testo in numeroint.parse("42") → Ok(42); string.trim prima
Aprire un Resultcase r { Ok(x) -> ... Error(e) -> ... }
Trasformare il valoreresult.map(r, fn(x) { ... })
Un altro passo che può fallireresult.try(r, fn(x) { ... })
Valore o ripiegoresult.unwrap(r, default)
Cambiare l’erroreresult.replace_error(r, MyError), result.map_error
Listelist.try_map (tutto o niente), list.filter_map (solo i successi), result.all
Catene dritteuse x <- result.try(operazione)
Uscita anticipatause <- bool.guard(when: cond, return: valore)
Codice da scriveretodo as "..."
Situazione impossibilepanic as "...", let assert Ok(x) = ...
Testpub 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:

gleam
pub type CommandError {
  UnknownCommand(String)
  BadSteps(String)
}

Poi scrivi:

  1. parse_command(word: String) -> Result(Command, CommandError): "L" e "R" sono le svolte; una parola che comincia con "F" è un Forward, se il resto è un numero maggiore di zero, altrimenti BadSteps con la parola intera; tutto il resto è UnknownCommand. (Ricordi il pattern "F" <> steps della lezione 2.5?)
  2. parse_program(text: String) -> Result(List(Command), CommandError): divide il testo sugli spazi, scarta i pezzi vuoti, e converte tutti i comandi con list.try_map.
  3. run_program(text: String) -> Result(Robot, CommandError): con use e result.try, legge il programma e, se è valido, lo esegue con list.fold partendo da (0, 0) verso nord.
  4. 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":

output
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: F0
Mostra una soluzione (prima prova da solo!)
src/robot_text.gleam
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ù). run e list.fold non sanno nulla degli errori: lavorano su comandi già validi.
  • list.try_map si ferma al primo comando sbagliato: in "F2 X F1" il robot non si muove nemmeno, perché il programma è rifiutato prima di partire.
  • In parse_command il pattern "F" <> steps separa la lettera dal numero, e la guardia Ok(n) if n > 0 scarta sia i numeri mancanti ("F") sia lo zero.
  • Tutte le funzioni che leggono sono pub: potresti scrivere dei test per parse_command in test/robot_text_test.gleam. Prova: assert parse_command("F3") == Ok(Forward(3)) e assert parse_command("Q") == Error(UnknownCommand("Q")) (con import 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.