Lezione 5 di 6 · 20 min di lettura

Dare forma a un programma

Dal progetto degli esercizi a un programma vero. Il modulo principale, le cartelle e i nomi dei moduli, quanti moduli scrivere, i moduli interni, la documentazione con gleam docs build, e un eseguibile da consegnare con gleam export escript.

Dall’esercizio al programma

Il progetto exercises è stato una palestra perfetta: un modulo per esercizio, ognuno lanciato con gleam run -m. Ma un programma vero è un’altra cosa: ha un nome, un punto d’ingresso, e il suo codice è diviso in moduli che collaborano. In questa lezione prepariamo lo scheletro del programma della sfida finale: shelf, uno scaffale di libri da usare dalla riga di comando.

Dalla cartella ~/learn-gleam (non da dentro exercises):

terminale
gleam new shelf
cd shelf
gleam add gleam_json simplifile argv

Il modulo principale

gleam new ha creato src/shelf.gleam: il modulo con lo stesso nome del progetto. È il modulo principale, quello che gleam run esegue quando non gli dici quale modulo usare (lezione 1.3). Il suo main è il punto d’ingresso del programma.

Il modulo principale dovrebbe restare sottile: legge gli argomenti, chiama le funzioni giuste degli altri moduli, stampa i risultati. Il lavoro vero sta altrove.

Le cartelle

Il resto del codice va in una cartella con lo stesso nome del progetto, src/shelf/. Un file src/shelf/book.gleam è il modulo shelf/book:

src/shelf/book.gleam
//// Books on the shelf.

import gleam/int

/// A book, with the year it was published.
pub type Book {
  Book(title: String, year: Int)
}

/// A short description, like `Dune (1965)`.
pub fn to_string(book: Book) -> String {
  book.title <> " (" <> int.to_string(book.year) <> ")"
}

e dal modulo principale si importa con il percorso completo, ma si usa con l’ultimo pezzo, come gleam/list si usa come list:

src/shelf.gleam
import gleam/io
import shelf/book.{Book}

pub fn main() -> Nil {
  let dune = Book(title: "Dune", year: 1965)
  io.println(book.to_string(dune))
}
output
Dune (1965)

Perché proprio una cartella con il nome del progetto? Perché in Gleam, come in tutto l’ecosistema della BEAM, i nomi dei moduli sono globali: se il tuo progetto avesse un modulo book, e un giorno aggiungessi un pacchetto che ha anche lui un modulo book, il progetto smetterebbe di compilare. Mettendo tutto sotto shelf/, il rischio sparisce. È la stessa ragione per cui i moduli della libreria standard stanno sotto gleam/; e un pacchetto piccolo come simplifile ha un solo modulo, con il nome del pacchetto.

Quanti moduli?

La guida ufficiale alle convenzioni dà tre consigli su come dividere il codice, e vale la pena seguirli fin da subito:

  • Nomi al singolare: shelf/book, non shelf/books. Anche nei pezzi intermedi: app/payment/invoice.
  • Dividi per argomento, non per categoria: un modulo per ogni “cosa” del tuo programma (i libri, il salvataggio su file), e mai moduli come types.gleam, utils.gleam o helpers.gleam, che raccolgono pezzi scollegati solo perché sono dello stesso genere.
  • Non frammentare: un modulo lungo non è un problema. Molti moduli piccoli, invece, costringono chi li usa a importarne cinque per fare una cosa semplice, e ti obbligano a rendere pubbliche funzioni che dovrebbero restare private. Dividi quando un pezzo ha davvero una vita sua.

Per lo scaffale basteranno tre moduli: shelf (la riga di comando), shelf/book (i libri) e shelf/storage (il file dove li salviamo).

I moduli interni

A volte un modulo serve agli altri moduli del progetto, ma non è pensato per chi usa il progetto come libreria. Per questi casi Gleam ha i moduli interni: di serie, sono tutti quelli sotto src/<progetto>/internal/, come shelf/internal/format. Si importano normalmente dal tuo codice, ma non compaiono nella documentazione, e non fanno parte di quello che il pacchetto promette: possono cambiare in qualsiasi momento, anche tra due versioni compatibili. Se ti serve un’altra regola, la scrivi in gleam.toml, con internal_modules.

Per un programma come shelf, che non verrà usato come libreria, non servono. Diventano importanti quando scriverai un pacchetto da pubblicare.

La documentazione

I commenti /// e //// che hai imparato nel modulo 1 non sono solo per chi legge il codice: gleam docs build li trasforma in un sito di documentazione, lo stesso formato di hexdocs.pm.

terminale
gleam docs build
output
 Generating documentation

The documentation for shelf has been rendered to
/home/ada/learn-gleam/shelf/build/dev/docs/shelf/index.html

Apri quel file nel browser (o lancia gleam docs build --open): c’è una pagina per ogni modulo, con i tipi e le funzioni pubbliche, i loro tipi, e i tuoi commenti. Le funzioni private non compaiono: sono affari del modulo. È un ottimo modo per vedere il tuo codice dal punto di vista di chi lo usa.

Consegnare il programma

Finora i programmi si lanciavano con gleam run, dalla cartella del progetto. Ma per usare shelf come uno strumento vero, o per darlo a un’altra persona, serve qualcosa di più comodo. gleam export escript impacchetta il programma, con tutte le sue dipendenze, in un unico file:

terminale
gleam export escript
output
Your escript has been generated to /home/ada/learn-gleam/shelf/shelf.

Il file shelf nella cartella del progetto è il programma completo, e si lancia direttamente, senza gleam run:

terminale
./shelf
output
Dune (1965)

Puoi copiarlo dove vuoi, anche su un altro computer: l’unica cosa che serve è che lì sia installato Erlang. Gli argomenti si passano normalmente, senza il --: ./shelf add Dune 1965.

Dettagli nerd Cos'è un escript, e perché serve comunque Erlang?

Un escript è un formato di Erlang per i programmi da riga di comando: un file che comincia con una riga speciale, #!/usr/bin/env escript, seguita dal bytecode compresso di tutti i moduli. Quella prima riga, lo shebang, dice al sistema operativo con quale programma aprire il file: qui, con escript, che fa parte di Erlang. Per questo il file è piccolo (qualche centinaio di KB), ma non basta da solo: la macchina virtuale che esegue il bytecode deve esserci.

Per i server e le applicazioni più grandi c’è gleam export erlang-shipment, che invece di un file unico prepara una cartella con il bytecode e uno script di avvio.

Quiz

Il progetto si chiama shelf. Dove metti il modulo che salva i libri su file, e come lo importi?

Esercizio · sul tuo computer

Lo scheletro dello scaffale

Nel progetto shelf (non in exercises):

  1. In src/shelf/book.gleam aggiungi al tipo Book un campo read: Bool, una funzione mark_read(book) -> Book che lo segna come letto, e cambia to_string perché metta davanti [x] se il libro è letto e [ ] se no. Scrivi un commento /// per ogni funzione pubblica.
  2. In src/shelf.gleam crea Dune (1965) ed Emma (1815), segna Emma come letto con book.mark_read, e stampa entrambi.
  3. Lancia gleam run, poi gleam docs build, e guarda come appaiono i tuoi commenti.
  4. Crea l’eseguibile con gleam export escript, e lancialo con ./shelf.
output
[ ] Dune (1965)
[x] Emma (1815)
Mostra una soluzione (prima prova da solo!)
src/shelf/book.gleam
//// Books on the shelf.

import gleam/int

/// A book, with the year it was published and whether it has been read.
pub type Book {
  Book(title: String, year: Int, read: Bool)
}

/// The same book, marked as read.
pub fn mark_read(book: Book) -> Book {
  Book(..book, read: True)
}

/// A one-line description, like `[x] Dune (1965)`.
pub fn to_string(book: Book) -> String {
  let mark = case book.read {
    True -> "[x] "
    False -> "[ ] "
  }
  mark <> book.title <> " (" <> int.to_string(book.year) <> ")"
}
src/shelf.gleam
import gleam/io
import shelf/book.{Book}

pub fn main() -> Nil {
  let dune = Book(title: "Dune", year: 1965, read: False)
  let emma = Book(title: "Emma", year: 1815, read: False) |> book.mark_read
  io.println(book.to_string(dune))
  io.println(book.to_string(emma))
}

mark_read usa la sintassi di aggiornamento dei record (lezione 4.3): restituisce un libro nuovo, uguale al vecchio tranne che per read.

Ricapitolando

  • Il modulo con il nome del progetto (src/shelf.gleam) è il principale: gleam run esegue il suo main. Tienilo sottile.
  • Gli altri moduli vanno sotto src/<progetto>/: src/shelf/book.gleam si importa con import shelf/book e si usa come book. I nomi dei moduli sono globali, e la cartella li protegge dalle collisioni.
  • Nomi al singolare, moduli divisi per argomento (mai utils o types), e senza frammentare.
  • I moduli sotto <progetto>/internal/ sono interni: non finiscono nella documentazione.
  • gleam docs build trasforma i commenti /// e //// in documentazione HTML.
  • gleam export escript crea un unico file eseguibile, che richiede solo Erlang.

Nella prossima lezione, la sfida finale: lo scaffale completo, con i libri salvati in JSON e i comandi dalla riga di comando.