Lezione 1 di 6 · 20 min di lettura
Parlare con Erlang
Usare da Gleam le funzioni scritte in Erlang. L'attributo @external, le annotazioni di tipo obbligatorie, un compilatore che deve fidarsi di te, i tipi esterni, e un file Erlang tutto tuo.
Trent’anni di librerie
Gleam gira sulla BEAM, e sulla BEAM c’è tanto altro: la libreria standard di Erlang, con centinaia di moduli collaudati da decenni, e tutto l’ecosistema di Elixir. Ogni tanto serve proprio una di quelle funzioni, e nessun pacchetto Gleam la offre già pronta.
Per questi casi Gleam ha le funzioni esterne: funzioni scritte in un altro linguaggio, che da Gleam si chiamano come quelle normali. Anzi, le usi da tempo senza saperlo: process.self() del modulo precedente è una funzione esterna, che chiama erlang:self().
@external
Una funzione esterna si dichiara con l’attributo @external e senza corpo:
import gleam/float
import gleam/io
import gleam/string
@external(erlang, "math", "sqrt")
fn square_root(x: Float) -> Float
@external(erlang, "lists", "reverse")
fn reverse_list(items: List(a)) -> List(a)
pub fn main() -> Nil {
io.println(float.to_string(square_root(2.0)))
io.println(string.inspect(reverse_list([1, 2, 3])))
}1.4142135623730951
[3, 2, 1]L’attributo ha tre argomenti:
- il bersaglio:
erlang(l’altro possibile èjavascript); - il modulo Erlang dove sta la funzione:
"math"; - il nome della funzione:
"sqrt".
Sotto c’è la firma della funzione Gleam, con il nome che preferisci (square_root, non sqrt) e i tipi. Da lì in poi square_root è una funzione come le altre: la chiami, la passi a list.map, la rendi pub se vuoi. Chiamarla non costa niente in più di una funzione scritta in Gleam: per la BEAM sono identiche.
Dettagli nerd Cos'è una FFI?
Il meccanismo che permette a un linguaggio di chiamare funzioni scritte in un altro si chiama FFI, Foreign Function Interface: interfaccia per le funzioni “straniere”. Quasi tutti i linguaggi ne hanno una: Python può chiamare codice C, Java ha la JNI, Rust può chiamare C e farsi chiamare da C.
Di solito è un confine faticoso: i due linguaggi rappresentano i dati in modo diverso, e bisogna convertirli ogni volta. Per Gleam ed Erlang il confine è quasi invisibile, perché Gleam diventa Erlang: un intero è un intero Erlang, una lista è una lista Erlang, una tupla è una tupla. Per questo nei progetti Gleam i file con le funzioni esterne si chiamano spesso qualcosa_ffi.erl.
I tipi sono obbligatori
In una funzione normale, il compilatore può dedurre i tipi guardando il corpo. Una funzione esterna non ha corpo, quindi i tipi vanno sempre scritti. Se ne manca uno:
error: Missing type annotation
┌─ /home/ada/learn-gleam/exercises/src/square_root.gleam:6:16
│
6 │ fn square_root(x) -> Float
│ ^
A parameter annotation is missing from this function.
Functions with external implementations must have type annotations
so we can tell what type of values they accept and return.Il compilatore si fida di te
Ecco il punto delicato. Il compilatore controlla che ogni chiamata a square_root rispetti la firma che hai scritto. Ma non può controllare che la firma sia vera: non legge il codice Erlang, e non sa nemmeno se la funzione esiste. Si fida.
Proviamo con erlang:system_info(otp_release), che dice quale versione di Erlang sta girando. L’argomento è l’atomo otp_release: in Gleam lo otteniamo con una variante senza dati, che sulla BEAM diventa proprio un atomo (lezione 4.2). E il risultato sembra una stringa:
import gleam/io
pub type Item {
OtpRelease
}
@external(erlang, "erlang", "system_info")
fn system_info(item: Item) -> String
pub fn main() -> Nil {
io.println("Running on Erlang/OTP " <> system_info(OtpRelease))
}runtime error: Erlang error
An error occurred outside of Gleam.
erlang:error(Badarg)
stacktrace:
otp_version.main src/otp_version.gleam:13
Compila senza un avviso, e poi si schianta. system_info non restituisce una stringa Gleam: restituisce una charlist, un altro modo di rappresentare il testo in Erlang. Il compilatore ci ha creduto, e il problema è emerso solo quando <> ha provato a concatenare due cose incompatibili: badarg, “argomento sbagliato”.
La soluzione è dire la verità. Il pacchetto gleam_erlang ha il tipo giusto, Charlist, e una funzione per convertirlo:
import gleam/erlang/charlist.{type Charlist}
import gleam/io
pub type Item {
OtpRelease
}
@external(erlang, "erlang", "system_info")
fn system_info(item: Item) -> Charlist
pub fn main() -> Nil {
let release = charlist.to_string(system_info(OtpRelease))
io.println("Running on Erlang/OTP " <> release)
}Running on Erlang/OTP 27(Il tuo numero può essere diverso: è la versione di Erlang installata sul tuo computer.)
Dettagli nerd Perché Erlang ha due tipi di stringhe?
Quando Erlang è nato, negli anni ‘80, il testo si rappresentava come una lista di numeri, uno per carattere: "ciao" era [99, 105, 97, 111]. È la charlist. Comoda da smontare con il pattern matching, ma costosa: ogni carattere è un elemento di una lista collegata, e occupa parecchi byte.
Più tardi Erlang ha aggiunto i binari: sequenze compatte di byte, dove una stringa in UTF-8 occupa più o meno un byte per carattere. Le stringhe di Gleam (e quelle di Elixir) sono binari. Molte funzioni della libreria di Erlang, però, sono più vecchie, e parlano ancora in charlist. Quando leggi la loro documentazione, string() vuol dire charlist, e binary() vuol dire una stringa come quelle di Gleam.
Quiz
Dichiari @external(erlang, "math", "sqrt") fn square_root(x: Int) -> String. Cosa succede?
I tipi esterni
A volte una funzione Erlang restituisce un valore che in Gleam non ha una forma: un riferimento, un handle a un file aperto, una connessione. Per questi casi si dichiara un tipo esterno: un tipo senza varianti.
pub type UniqueId
@external(erlang, "erlang", "make_ref")
pub fn new_id() -> UniqueIdGleam sa solo che UniqueId esiste: non può costruirne uno né guardarci dentro, e l’unico modo di averne uno è new_id(). Funziona come un tipo opaco, ed è così che gleam_erlang definisce Pid: pub type Pid, e basta.
Dare a ogni valore esterno il suo tipo, invece di un tipo generico, è un consiglio della guida ufficiale: se un handle di un file e un handle di una connessione hanno tipi diversi, il compilatore non ti lascerà mai confonderli.
Un file Erlang tutto tuo
Le funzioni esterne possono stare anche in un file Erlang del tuo progetto. Metti un file .erl dentro src/, accanto ai moduli Gleam, e Gleam lo compila insieme a loro:
-module(dice_ffi).
-export([roll/1]).
roll(Sides) ->
rand:uniform(Sides).In Erlang, -module dà il nome al modulo (deve essere uguale al nome del file), -export elenca le funzioni visibili da fuori con il numero di argomenti, e rand:uniform(N) è un intero a caso tra 1 e N. Da Gleam:
import gleam/int
import gleam/io
@external(erlang, "dice_ffi", "roll")
fn roll(sides: Int) -> Int
pub fn main() -> Nil {
io.println("You rolled " <> int.to_string(roll(6)))
}Ogni volta che lo lanci, un numero diverso: You rolled 4, You rolled 1…
E se sbagli il nome della funzione, scrivendo "rol" invece di "roll"? Di nuovo, il compilatore si fida, e l’errore arriva solo a tempo di esecuzione:
import gleam/int
import gleam/io
@external(erlang, "dice_ffi", "rol")
fn roll(sides: Int) -> Int
pub fn main() -> Nil {
io.println("You rolled " <> int.to_string(roll(6)))
}runtime error: Erlang error
A function was called but it did not exist.
stacktrace:
dice_ffi.rol unknown source
dice.main src/dice.gleam:10
Più bersagli, e il piano B
Una funzione può avere un @external per ogni bersaglio, uno sotto l’altro:
@external(erlang, "lists", "reverse")
@external(javascript, "./my_ffi.mjs", "reverse_list")
pub fn reverse_list(items: List(a)) -> List(a)E può avere anche un corpo in Gleam: in quel caso l’@external si usa sui bersagli per cui c’è, e il corpo su tutti gli altri. È utile quando una funzione si può scrivere in Gleam, ma Erlang ne ha una versione più veloce.
Un consiglio dalla guida ufficiale: usa le funzioni esterne con parsimonia. Il codice che non è Gleam il compilatore non lo controlla, e ogni @external è un posto dove i tipi potrebbero mentire. Prima di scriverne una, cerca su packages.gleam.run: spesso qualcuno ha già fatto il lavoro, e l’ha provato.
Esercizio · sul tuo computer
Quattro funzioni da Erlang
Crea src/from_erlang.gleam e dichiara quattro funzioni esterne:
pi() -> Float, damath:pi/0;power(base: Float, exponent: Float) -> Float, damath:pow/2;sequence(from: Int, to: Int) -> List(Int), dalists:seq/2, che restituisce gli interi dafromatocompresi;uppercase(text: String) -> String, dastring:uppercase/1(che accetta anche i binari, quindi le stringhe di Gleam).
In main stampa pi(), power(2.0, 10.0), sequence(1, 5) e uppercase("Hello, Joe!"):
3.141592653589793
1024.0
[1, 2, 3, 4, 5]
HELLO, JOE!Mostra una soluzione (prima prova da solo!)
import gleam/float
import gleam/io
import gleam/string
@external(erlang, "math", "pi")
fn pi() -> Float
@external(erlang, "math", "pow")
fn power(base: Float, exponent: Float) -> Float
@external(erlang, "lists", "seq")
fn sequence(from: Int, to: Int) -> List(Int)
@external(erlang, "string", "uppercase")
fn uppercase(text: String) -> String
pub fn main() -> Nil {
io.println(float.to_string(pi()))
io.println(float.to_string(power(2.0, 10.0)))
io.println(string.inspect(sequence(1, 5)))
io.println(uppercase("Hello, Joe!"))
}Il /1 e il /2 nella documentazione di Erlang sono il numero di argomenti: in Erlang lists:seq/2 e lists:seq/3 sono due funzioni diverse, e l’@external sceglie quella con tanti argomenti quanti ne ha la firma Gleam.
Ricapitolando
@external(erlang, "modulo", "funzione")sopra una funzione senza corpo la collega a una funzione Erlang.- I tipi dei parametri e del risultato sono obbligatori.
- Il compilatore si fida della firma: se è sbagliata, o se la funzione non esiste, l’errore arriva quando il programma gira.
- Molte funzioni Erlang usano le charlist, non le stringhe di Gleam:
gleam/erlang/charlistle converte. - Un tipo esterno (
pub type UniqueId, senza varianti) rappresenta un valore che Gleam non può guardare dentro. - I file
.erlinsrc/vengono compilati insieme al progetto. - Una funzione può avere più
@external(uno per bersaglio) e un corpo Gleam di riserva. Usale con parsimonia.
Nella prossima lezione vedremo il modo sicuro di ricevere dati dal mondo esterno: non fidarsi, e controllare.