Lezione 7 di 7 · 25 min di lettura

Riepilogo e sfida finale

Processi, messaggi, attori e supervisori in una pagina, tre domande di controllo, e una biglietteria presa d'assalto da tanti clienti nello stesso istante.

Guarda quanta strada

In questo modulo i tuoi programmi hanno smesso di fare una cosa alla volta. Adesso sai:

  • aggiungere pacchetti al progetto con gleam add, e perché manifest.toml si salva con il codice;
  • avviare processi con process.spawn, e ricordarti che l’ordine tra processi non è garantito;
  • farli parlare con Subject, send e receive, con un tipo controllato dal compilatore per ogni messaggio;
  • tenere uno stato dentro un processo, e chiedergli qualcosa con process.call;
  • scrivere attori con gleam/otp/actor, nascosti dietro le funzioni di un modulo;
  • lasciare che un processo si schianti, e farlo ripartire con un supervisore e un nome.

La pagina da tenere accanto

CosaCome si scrive
Aggiungere un pacchettogleam add gleam_erlang gleam_otp
Le dipendenzegleam deps tree, gleam deps list, gleam remove, gleam update
Una pausaprocess.sleep(1000) (millisecondi)
Un processoprocess.spawn(fn() { ... }) → Pid; process.self()
Un indirizzolet inbox = process.new_subject(); tipo Subject(String)
Spedireprocess.send(inbox, "Hello")
Ricevereprocess.receive(inbox, within: 1000) → Ok(m) o Error(Nil); process.receive_forever(inbox)
Chiedere e aspettareprocess.call(subject, waiting: 100, sending: Get)
Un attoreactor.new(stato) \|> actor.on_message(gestore) \|> actor.start
Il gestorefn(stato, messaggio) -> actor.Next(stato, messaggio)
Come proseguireactor.continue(nuovo_stato), actor.stop()
Un nomeprocess.new_name("counter"), actor.named(name), process.named_subject(name)
Un supervisoresupervisor.new(supervisor.OneForOne) \|> supervisor.add(supervision.worker(avvio)) \|> supervisor.start
Senza linkprocess.spawn_unlinked(fn() { ... })

Trovi tutto anche nel Codex (tasto K).

Quiz di controllo

Quiz

Hai scritto import gleam/otp/actor, ma il compilatore risponde Unknown module. Qual è la causa più probabile?

Quiz

Un processo figlio, avviato con process.spawn, si schianta mentre main sta aspettando con process.sleep. Cosa succede a main?

Quiz

Mille processi mandano Increment allo stesso attore contatore nello stesso istante. Quanto vale il contatore alla fine?

La sfida: la biglietteria

Stanno per aprire le vendite per un concerto, in una sala con 10 posti, numerati da 1 a 10. Nello stesso istante, otto fan si lanciano sulla biglietteria, e ognuno prova a prenotare tutti i posti, dal primo all’ultimo. Il problema classico: due fan che chiedono lo stesso posto nello stesso istante non devono ottenerlo entrambi.

Con un attore il problema si risolve da solo: la biglietteria è l’unico processo che conosce le prenotazioni, e gestisce le richieste una alla volta. Chiunque arrivi secondo, anche per un microsecondo, trova il posto già preso.

Esercizio · sul tuo computer

La biglietteria

Nel progetto exercises crea due moduli.

src/box_office.gleam è l’attore della biglietteria:

  • un tipo di errore pubblico BookingError, con NoSuchSeat e AlreadyTaken(by: String);
  • un tipo opaco Message, con Book(seat: Int, customer: String, reply_to: ...) e Bookings(reply_to: ...);
  • lo stato: il numero di posti e un Dict(Int, String) da posto a cliente;
  • start(seats: Int) -> Result(Subject(Message), actor.StartError);
  • book(office, seat: Int, for customer: String) -> Result(Nil, BookingError): un posto fuori dalla sala è NoSuchSeat, un posto già prenotato è AlreadyTaken con il nome di chi l’ha preso;
  • bookings(office) -> Dict(Int, String): tutte le prenotazioni.

src/concert.gleam apre una biglietteria da 10 posti. Prima, con calma, Ada prenota il posto 1, poi Joe prova a prendere il posto 1 e il posto 11, e stampa il risultato di ogni tentativo. Poi avvia otto processi, i fan (fan 1, …, fan 8): ognuno prova a prenotare i posti da 1 a 10, e alla fine manda a main una coppia con il numero di prenotazioni riuscite e rifiutate. main somma le coppie degli otto fan, e stampa il totale e il numero di posti venduti:

output
Ada: seat 1 booked
Joe: seat 1 is taken by Ada
Joe: seat 11 does not exist
Fans booked 9 seats, 71 requests refused
Seats sold: 10 of 10

Lancia il programma più volte: chi prende quali posti cambia ogni volta, ma i totali no. Se fossero diversi, due fan avrebbero lo stesso posto.

Mostra una soluzione (prima prova da solo!)
src/box_office.gleam
import gleam/dict.{type Dict}
import gleam/erlang/process.{type Subject}
import gleam/otp/actor
import gleam/result

pub type BookingError {
  NoSuchSeat
  AlreadyTaken(by: String)
}

pub opaque type Message {
  Book(
    seat: Int,
    customer: String,
    reply_to: Subject(Result(Nil, BookingError)),
  )
  Bookings(reply_to: Subject(Dict(Int, String)))
}

type State {
  State(seats: Int, bookings: Dict(Int, String))
}

/// Starts a box office for a hall with seats numbered from 1 to `seats`.
pub fn start(seats: Int) -> Result(Subject(Message), actor.StartError) {
  actor.new(State(seats:, bookings: dict.new()))
  |> actor.on_message(handle_message)
  |> actor.start
  |> result.map(fn(started) { started.data })
}

pub fn book(
  office: Subject(Message),
  seat seat: Int,
  for customer: String,
) -> Result(Nil, BookingError) {
  actor.call(office, waiting: 100, sending: fn(reply_to) {
    Book(seat:, customer:, reply_to:)
  })
}

pub fn bookings(office: Subject(Message)) -> Dict(Int, String) {
  actor.call(office, waiting: 100, sending: Bookings)
}

fn handle_message(
  state: State,
  message: Message,
) -> actor.Next(State, Message) {
  case message {
    Book(seat:, customer:, reply_to:) ->
      case check(state, seat) {
        Ok(Nil) -> {
          actor.send(reply_to, Ok(Nil))
          let bookings = dict.insert(state.bookings, seat, customer)
          actor.continue(State(..state, bookings:))
        }
        Error(error) -> {
          actor.send(reply_to, Error(error))
          actor.continue(state)
        }
      }
    Bookings(reply_to) -> {
      actor.send(reply_to, state.bookings)
      actor.continue(state)
    }
  }
}

fn check(state: State, seat: Int) -> Result(Nil, BookingError) {
  case dict.get(state.bookings, seat) {
    _ if seat < 1 || seat > state.seats -> Error(NoSuchSeat)
    Ok(owner) -> Error(AlreadyTaken(by: owner))
    Error(Nil) -> Ok(Nil)
  }
}
src/concert.gleam
import box_office.{type BookingError, type Message, AlreadyTaken, NoSuchSeat}
import gleam/dict
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/io

pub fn main() -> Nil {
  let assert Ok(office) = box_office.start(10)
  report("Ada", 1, box_office.book(office, seat: 1, for: "Ada"))
  report("Joe", 1, box_office.book(office, seat: 1, for: "Joe"))
  report("Joe", 11, box_office.book(office, seat: 11, for: "Joe"))

  let results = process.new_subject()
  int.range(from: 1, to: 9, with: Nil, run: fn(_, n) {
    let fan = "fan " <> int.to_string(n)
    process.spawn(fn() { process.send(results, try_all_seats(office, fan)) })
    Nil
  })
  let #(booked, refused) = collect(results, 8, #(0, 0))
  io.println(
    "Fans booked "
    <> int.to_string(booked)
    <> " seats, "
    <> int.to_string(refused)
    <> " requests refused",
  )
  let sold = dict.size(box_office.bookings(office))
  io.println("Seats sold: " <> int.to_string(sold) <> " of 10")
}

fn report(
  customer: String,
  seat: Int,
  result: Result(Nil, BookingError),
) -> Nil {
  let seat = "seat " <> int.to_string(seat)
  case result {
    Ok(Nil) -> io.println(customer <> ": " <> seat <> " booked")
    Error(NoSuchSeat) ->
      io.println(customer <> ": " <> seat <> " does not exist")
    Error(AlreadyTaken(by:)) ->
      io.println(customer <> ": " <> seat <> " is taken by " <> by)
  }
}

/// Tries to book every seat, and returns how many bookings succeeded and failed.
fn try_all_seats(office: Subject(Message), fan: String) -> #(Int, Int) {
  int.range(from: 1, to: 11, with: #(0, 0), run: fn(acc, seat) {
    case box_office.book(office, seat:, for: fan) {
      Ok(Nil) -> #(acc.0 + 1, acc.1)
      Error(_) -> #(acc.0, acc.1 + 1)
    }
  })
}

fn collect(
  results: Subject(#(Int, Int)),
  left: Int,
  total: #(Int, Int),
) -> #(Int, Int) {
  case left {
    0 -> total
    _ -> {
      let #(booked, refused) = process.receive_forever(results)
      collect(results, left - 1, #(total.0 + booked, total.1 + refused))
    }
  }
}

Qualche dettaglio da notare:

  • book ha bisogno di passare al messaggio anche il posto e il cliente, quindi sending non può essere solo Book: è una funzione anonima che riceve il Subject della risposta e costruisce il messaggio completo, con la scorciatoia delle etichette (Book(seat:, customer:, reply_to:)).
  • In check, la prima clausola _ if seat < 1 || seat > state.seats scatta qualunque sia il risultato di dict.get, come in sell nella sfida del modulo 6.
  • Message è opaco, ma BookingError no: concert può fare pattern matching sugli errori, importando le varianti per nome.
  • Ogni fan è un processo, e try_all_seats gira dentro di lui: le 80 richieste partono davvero in parallelo, e l’attore le mette in fila. 71 rifiutate: le 8 per il posto 1, che era già di Ada, più le 63 arrivate tardi per gli altri nove posti.
  • collect è la solita ricorsione con accumulatore, questa volta con una tupla: una coppia di somme.

Cosa succede nel Modulo 8

Con questo modulo hai visto il cuore della BEAM, e il motivo per cui Gleam ha scelto di vivere lì. Ma la BEAM non è fatta solo di Gleam: ci sono trent’anni di librerie Erlang, e tutto l’ecosistema di Elixir.

Nel prossimo modulo vedremo come Gleam parla con il mondo esterno: chiamare direttamente funzioni di Erlang con @external, leggere dati di cui non conosciamo il tipo con i decoder di gleam/dynamic, e come si organizza un programma Gleam più grande di un esercizio.