Lezione 6 di 6 · 22 min di lettura

Riepilogo e sfida finale

I tipi del modulo in una pagina, tre domande di controllo, e un robot su una griglia costruito con varianti, record e fold.

Guarda quanta strada

In questo modulo hai smesso di usare solo i tipi che Gleam ti dava, e hai cominciato a costruire i tuoi. Adesso sai:

  • raggruppare valori al volo con le tuple, e restituirne più d’uno da una funzione;
  • definire tipi con varianti, e lasciare al compilatore il controllo di ogni case;
  • costruire record con i campi con un nome, leggerli, smontarli e aggiornarli;
  • combinare varianti e dati, e scegliere i tipi in modo che gli stati senza senso non si possano scrivere;
  • scrivere tipi generici, e rappresentare un valore che potrebbe mancare con Option.

La pagina da tenere accanto

CosaCome si scrive
Tupla#(3, "three"), tipo #(Int, String)
Elemento di una tuplapair.0, pair.1
Smontare una tuplalet #(a, b) = pair
Coppie da due listelist.zip(a, b), list.unzip(pairs)
Tipo con variantipub type Season { Spring Summer Autumn Winter }
Recordpub type Person { Person(name: String, age: Int) }
CostruirePerson(name: "Ada", age: 36)
Leggere un campoada.name
Smontarelet Person(name:, ..) = ada, o in un case
AggiornarePerson(..ada, age: 37)
Varianti con datiCircle(radius: Float), Rectangle(width: Float, height: Float)
Alternative tra variantiCash(cents:) \| Card(cents:, ..) -> cents
Tipo genericopub type Maybe(a) { Just(a) Nothing }
Valore che può mancareOption(a): Some(x), None
Importare per nomeimport gleam/option.{type Option, None, Some}
Aliaspub type Cents = Int

Trovi tutto anche nel Codex (tasto K).

Quiz di controllo

Quiz

Con let #(a, b) = divmod(7, 2) e fn divmod(x: Int, y: Int) -> #(Int, Int) { #(x / y, x % y) }, quanto valgono a e b?

Quiz

Aggiungi una variante Triangle(base: Float, height: Float) al tipo Shape. Cosa succede alla funzione area, che ha un case con una clausola per ogni altra forma?

Quiz

Con pub type User { Guest LoggedIn(name: String) } e un user: User, cosa si scrive per ottenerne il nome?

La sfida: il robot

Un piccolo robot si muove su una griglia. Parte dalla casella (0, 0), guardando a nord, e riceve una lista di comandi: avanti di un certo numero di caselle, gira a sinistra, gira a destra. Andare verso nord aumenta y, verso sud la diminuisce; verso est aumenta x, verso ovest la diminuisce. Girare cambia solo la direzione, non la posizione.

Esercizio · sul tuo computer

Il robot

Nel progetto exercises crea src/robot.gleam con tre tipi:

  • Direction, con le quattro direzioni North, East, South, West;
  • Robot, un record con x, y (interi) e facing (una Direction);
  • Command, con le varianti Forward(steps: Int), TurnLeft e TurnRight.

Poi le funzioni:

  1. turn_left e turn_right, da una Direction alla nuova direzione;
  2. run(robot: Robot, command: Command) -> Robot, che esegue un comando e restituisce il robot aggiornato (usa la sintassi Robot(..robot, ...));
  3. describe(robot: Robot) -> String, come (2, 3) facing East, e command_name(command: Command) -> String, come Forward 3 o Turn right;
  4. distance(from: Robot, to: Robot) -> Int, la distanza “a isolati” tra due posizioni: la differenza delle x più la differenza delle y, in valore assoluto (int.absolute_value).

In main il robot riceve i comandi [Forward(3), TurnRight, Forward(2), TurnRight, Forward(5), TurnLeft, Forward(1)]. Esegui la lista con un list.fold che parte dal robot iniziale e, a ogni passo, stampa il comando e la nuova posizione. Alla fine stampa la distanza dal punto di partenza:

output
Start: (0, 0) facing North
Forward 3 -> (0, 3) facing North
Turn right -> (0, 3) facing East
Forward 2 -> (2, 3) facing East
Turn right -> (2, 3) facing South
Forward 5 -> (2, -2) facing South
Turn left -> (2, -2) facing East
Forward 1 -> (3, -2) facing East
Distance: 5

Suggerimento: la funzione del fold fa tre cose. Calcola il nuovo robot con un let, lo stampa, e lo restituisce come nuovo accumulatore.

Mostra una soluzione (prima prova da solo!)
src/robot.gleam
import gleam/int
import gleam/io
import gleam/list

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 fn main() -> Nil {
  let start = Robot(x: 0, y: 0, facing: North)
  let commands = [
    Forward(3),
    TurnRight,
    Forward(2),
    TurnRight,
    Forward(5),
    TurnLeft,
    Forward(1),
  ]

  io.println("Start: " <> describe(start))
  let finish =
    list.fold(commands, start, fn(robot, command) {
      let next = run(robot, command)
      io.println(command_name(command) <> " -> " <> describe(next))
      next
    })
  io.println("Distance: " <> int.to_string(distance(start, finish)))
}

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 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"
  }
}

fn command_name(command: Command) -> String {
  case command {
    Forward(steps:) -> "Forward " <> int.to_string(steps)
    TurnLeft -> "Turn left"
    TurnRight -> "Turn right"
  }
}

fn distance(from: Robot, to: Robot) -> Int {
  int.absolute_value(to.x - from.x) + int.absolute_value(to.y - from.y)
}

Qualche dettaglio da notare:

  • Nessuna variabile cambia: ogni comando produce un robot nuovo, e il fold passa il più recente al comando successivo. Il robot di partenza, start, è ancora lì intatto alla fine, ed è per questo che si può calcolare la distanza.
  • run ha un case dentro un case: prima il comando, poi, per Forward, la direzione. Ogni combinazione possibile è gestita, e il compilatore lo verifica.
  • Stampare dentro la funzione del fold funziona, ma mescola il calcolo con l’output. Un’alternativa più pulita: calcolare prima la lista di tutte le posizioni (con list.scan, che è come fold ma restituisce tutti gli accumulatori intermedi) e stamparla dopo.

Cosa succede nel Modulo 5

In questo modulo abbiamo sempre scritto noi i dati: la lista delle persone, i comandi del robot. Ma i programmi veri ricevono dati dall’esterno, e i dati esterni possono essere sbagliati: un numero che non è un numero, un comando sconosciuto, un file che non c’è. Come fa un linguaggio senza eccezioni a gestire gli errori?

Nel prossimo modulo scopriremo il tipo Result, con le sue varianti Ok ed Error, il modulo gleam/result per concatenare operazioni che possono fallire, e la parola chiave use, che rende questo codice sorprendentemente leggibile. E potremo finalmente scrivere un robot che legge i comandi da un testo come "F3 R F2".