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.tomlsi salva con il codice; - avviare processi con
process.spawn, e ricordarti che l’ordine tra processi non è garantito; - farli parlare con
Subject,sendereceive, 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
| Cosa | Come si scrive |
|---|---|
| Aggiungere un pacchetto | gleam add gleam_erlang gleam_otp |
| Le dipendenze | gleam deps tree, gleam deps list, gleam remove, gleam update |
| Una pausa | process.sleep(1000) (millisecondi) |
| Un processo | process.spawn(fn() { ... }) → Pid; process.self() |
| Un indirizzo | let inbox = process.new_subject(); tipo Subject(String) |
| Spedire | process.send(inbox, "Hello") |
| Ricevere | process.receive(inbox, within: 1000) → Ok(m) o Error(Nil); process.receive_forever(inbox) |
| Chiedere e aspettare | process.call(subject, waiting: 100, sending: Get) |
| Un attore | actor.new(stato) \|> actor.on_message(gestore) \|> actor.start |
| Il gestore | fn(stato, messaggio) -> actor.Next(stato, messaggio) |
| Come proseguire | actor.continue(nuovo_stato), actor.stop() |
| Un nome | process.new_name("counter"), actor.named(name), process.named_subject(name) |
| Un supervisore | supervisor.new(supervisor.OneForOne) \|> supervisor.add(supervision.worker(avvio)) \|> supervisor.start |
| Senza link | process.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, conNoSuchSeateAlreadyTaken(by: String); - un tipo opaco
Message, conBook(seat: Int, customer: String, reply_to: ...)eBookings(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 èAlreadyTakencon 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:
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 10Lancia 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!)
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)
}
}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:
bookha bisogno di passare al messaggio anche il posto e il cliente, quindisendingnon può essere soloBook: è una funzione anonima che riceve ilSubjectdella 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.seatsscatta qualunque sia il risultato didict.get, come insellnella sfida del modulo 6. Messageè opaco, maBookingErrorno:concertpuò fare pattern matching sugli errori, importando le varianti per nome.- Ogni fan è un processo, e
try_all_seatsgira 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.