Erlang è un linguaggio di programmazione nato nei primi anni '90 dalle esigenze di Ericsson per quanto riguarda lo sviluppo di sistemi di telecomunicazione, come soluzione a requisiti stringenti di scalabilità, concorrenza,
fault-tolerance e computazione distribuita in sistemi soft real-time. Queste caratteristiche e il supporto per l'SMP rendono inoltre Erlang una scelta valida per sfruttare appieno la potenza di calcolo di macchine multicore.
Questo libro, dal grande
valore didattico, anche perché scaturito direttamente dal materiale utilizzato in dieci anni di insegnamento, vede come
autori Francesco Cesarini, fondatore di
Erlang Training and Consulting, e
Simon Thompson, docente dell'Università di Kent e autore di altri testi significativi, tra cui
Haskell: The Craft of Functional Programming.
Il target è costituito da programmatori – anche privi di conoscenze pregresse sulla programmazione funzionale, che comunque non è trattata in modo molto intensivo – desiderosi di imparare Erlang
ex novo, pur dando per scontata l'esperienza con un altro linguaggio di programmazione, come ad esempio C, Java o Ruby. Coloro in cerca di una introduzione
delicata potrebbero non rimanere soddisfatti, dato il passo celere nel dispensare le nozioni già fin dal principio.
La trattazione segue il modello classico: i concetti e i costrutti sono introdotti ed esplorati attraverso
esempi significativi di codice, soggetti alla spiegazione e all'analisi degli autori, con eventuali immagini a supporto. Il modo migliore per apprendere quanto esposto consiste appunto nel rieseguire, passo dopo passo, il codice e i ragionamenti relativi; dopodiché, è possibile verificare la propria comprensione svolgendo gli
esercizi forniti al termine di ogni capitolo, i quali consistono nel modificare o estendere il codice fornito negli esempi. Il
codice sorgente è liberamente scaricabile dal
sito del libro.
Il contenuto è diviso in due parti, per un'organizzazione efficace ed equilibrata:
i primi 11 capitoli sono stati pensati per una lettura sequenziale, e descrivono gli aspetti fondamentali della programmazione in Erlang; i restanti capitoli sono dedicati ad argomenti sostanzialmente indipendenti, che si prestano pertanto anche a una lettura random. Per la natura stessa dei temi discussi nella seconda parte, non è stato possibile trattare tutto esaurientemente, per cui al termine di questi capitoli è in genere presente un paragrafo del tipo
Further Reading, per segnalare riferimenti a informazioni aggiuntive.
L'
appendice A,
Using Erlang, raccoglie i dettagli sull'installazione del runtime, sull'esecuzione di programmi Erlang, e su strumenti che potrebbero essere utili, nonché ulteriori riferimenti da cui attingere informazioni.
Il
primo capitolo è introduttivo e cerca di indirizzare il lettore verso le idee e i contenuti esposti. L'obiettivo è far comprendere ciò che Erlang può offrire e in quali casi il suo utilizzo può portare vantaggi significativi. Dunque si ripercorre la
storia del linguaggio, si indicano le
caratteristiche principali che mette a disposizione, e si analizzano due
case study, per chiudere il capitolo con un confronto, dati alla mano, tra Erlang e C++.
Il
secondo capitolo inizia a dare i primi strumenti per gli aspiranti programmatori Erlang, servendosi, come in tutto il libro, della shell interattiva
erl con il suo
REPL (Read-Evaluate-Print Loop). In particolare, vengono introdotti, assieme alla terminologia relativa, i tipi di dato fondamentali, le operazioni elementari sulle liste, i confronti, le variabili, i termini Erlang, le funzioni, i moduli e la loro compilazione, e il
pattern matching. Quest'ultimo specialmente è molto importante, poiché consente di assegnare valori alle variabili (che sono a singolo assegnamento), di estrarre valori da tipi di dato composti e di controllare il flusso di esecuzione dei programmi; infatti, il
pattern matching dei parametri attuali, con le varie intestazioni di una funzione, seleziona la giusta clausola da eseguire.
Il controllo del flusso è approfondito nel
capitolo 3,
Sequential Erlang, con la descrizione dei costrutti
case,
if, e delle
guard expression, con cui si possono aggiungere vincoli addizionali all'esecuzione di una certa clausola. Successivamente vengono presentate le
BIF (Built-In Function) più utilizzate, per poi affrontare la ricorsione, gli errori runtime e la loro gestione mediante il costrutto
try..catch, i moduli di libreria e l'utilizzo del debugger.
Il
capitolo 4 getta le basi del
modello di concorrenza. Ogni attività concorrente viene detta processo; tra processi non vi è condivisione di dati in memoria, ed essi comunicano scambiandosi messaggi.
La trasmissione dei messaggi è asincrona, e ogni processo ha una
mailbox da cui è possibile recuperare i messaggi tramite la clausola
receive. La gestione e lo scheduling dei processi sono a carico della macchina virtuale Erlang; in particolare, non viene generato un thread del sistema operativo per ogni processo creato. Oltre alle dinamiche relative al passaggio di messaggi, viene trattata la registrazione dei processi – per riferirsi a essi attraverso un alias, piuttosto che con il loro
pid – i timeout, il process manager, il benchmarking e sono presenti considerazioni a proposito di
race condition e
deadlock.
Il
capitolo 5,
Process Design Patterns, analizza, con tanto di esempi,
pattern tipici di programmazione concorrente: client/server,
FSM (Finite State Machine) ed
event handler. Viene giustamente sottolineata la necessità di astrarre e nascondere le informazioni sulle risorse e sul paradigma di comunicazione dietro a un'interfaccia funzionale.
Nel
capitolo 6, con la gestione degli errori tra processi, si completa il quadro sulle fondamenta della concorrenza in Erlang. Tra i meccanismi forniti, vi è la possibilità di stabilire collegamenti bidirezionali (link) e unidirezionali (monitor), su cui avviene la propagazione dei segnali d'uscita, messaggi inviati da un processo a seguito di una terminazione non corretta verso i suoi collegamenti. Normalmente nei link i segnali d'uscita provocano la terminazione dei processi collegati, ma impostando il flag
trap_exit, è possibile fare in modo che questi segnali vengano convertiti in messaggi nel formato
{'EXIT', Pid, Reason} e ricevuti nella
mailbox per gestirli. Nella costruzione di sistemi robusti si effettua poi un
layering per isolare la propagazione degli errori all'interno di un singolo
supervisor tree.
Record e macro sono l'oggetto del
capitolo 7. Le strutture dati a record, create con l'omonima direttiva, forniscono un'astrazione, che regala la flessibilità che non è fornita dagli altri tipi composti; ciò è ottenuto nascondendo la rappresentazione reale e uniformando così l'accesso ai dati. Le macro, invece, consistono in abbreviazioni, che vengono espanse dal preprocessore. Record e macro apportano un contributo in termini di manutenzione di programmi e vengono solitamente posti in file
.hrl da caricare con la direttiva
include.
La capacità di caricare aggiornamenti software a runtime (software upgrade) senza dover interrompere l'esecuzione del sistema è di grande importanza, soprattutto laddove sono da limitare al minimo i tempi di inattività. Questo, con tanto di esempi e di spiegazioni su ciò che accade dietro le quinte, è descritto nel
capitolo 8.
Il
capitolo 9 tratta alcuni elementi ereditati dal mondo funzionale. Gli oggetti-funzione costituiscono un tipo di dato e sono detti
fun; essi possono essere associati a variabili, passati alle funzioni o restituiti dalle funzioni come valore di ritorno. Le funzioni, che hanno a che fare con le
fun sono dette
higher-order function; ce ne sono alcune presenti nel modulo
lists che vale la pena conoscere, come ad esempio
filter,
map,
foldl. Le
list-comprehension sono delle notazioni molto compatte e concise per costruire liste applicando funzioni agli elementi di altre liste opportunamente filtrate. Inoltre si parla dei
binary – strutture dati binarie definite mediante una particolare notazione, che supporta il
pattern matching e la definizione di segmenti con tanto di tipo e dimensione in bit – e dei
ref, etichette per identificare nodi in un ambiente distribuito.
A volte si ha l'esigenza di memorizzare grandi quantità di dati e al tempo stesso mantenere un accesso ai dati efficiente. Allo scopo, si utilizzano le tabelle
ETS (Erlang Term Storage), che immagazzinano tuple e forniscono l'accesso attraverso un campo chiave. Queste tabelle possono essere di quattro tipi:
set, con chiavi uniche;
bag, con chiavi multiple ed elementi distinti;
duplicate bag, nel caso si desideri avere chiavi uguali con elementi uguali; e
ordered set, con le chiavi ordinate per nome. I primi tre tipi di tabella sono implementati con tabelle hash e quindi consentono un tempo d'accesso costante; gli
ordered set sono invece implementati con alberi binari bilanciati, e si hanno tempi d'accesso che crescono con il logaritmo della dimensione. Quando si tratta di ottenere una memorizzazione efficiente su disco, le tabelle
DETS (Disk Erlang Term Storage) vengono in aiuto. Il
capitolo 10 approfondisce l'argomento e termina con un esempio completo relativo a un database degli abbonati di un provider di telefonia mobile.
E' apprezzabile l'efficacia, la concisione e il dettaglio con cui è stato esposto il nucleo del linguaggio. Con il
capitolo 11, relativo alla
programmazione distribuita – feature implementata come parte del linguaggio – si chiude questo primo ciclo. La fornitura di un servizio in maniera trasparente attraverso più nodi Erlang, che possono risiedere sulla stessa macchina o su computer diversi e sono identificati da un nome, è fondamentale per ottenere sistemi scalabili e affidabili. La comunicazione e un primo banale livello di sicurezza tra nodi sono dati da un cookie segreto, che dev'essere conosciuto dagli interlocutori. Una tipica applicazione consiste nel paradigma
RPC (Remote Procedure Call).
Il
capitolo 12 offre una panoramica degli
OTP Behavior, funzionalità fornite da moduli di libreria, che formalizzano i process design pattern, e con una gestione consistente degli errori, attrezzano il programmatore con una serie di componenti, che possono essere impiegati per sviluppare efficacemente applicazioni lascamente accoppiate ed eventualmente distribuite. Sono messi a disposizione sia processi
worker, che svolgono l'elaborazione, sia processi
supervisor, che svolgono il monitoraggio dei processi figli; essi sono poi raggruppati in alberi di supervisione, che insieme andranno a costituire una'
applicazione OTP. In pratica, si hanno a disposizione dei comportamenti generici, come ad esempio client/server o
supervision, da specializzare definendo moduli di
callback. Le macro componenti dei sistemi Erlang sono dunque applicazioni normali – se lanciano un albero di supervisione e
worker statici – e di libreria, a basso accoppiamento, specificate in un file di release.
Mnesia è un
DBMS distribuito, molto adatto per applicazioni super concorrenti, in cui sono richiesti alti livelli di scalabilità e affidabilità; esso consiste sostanzialmente in tabelle
ETS e
DETS integrate con un livello per le transazioni. Il
capitolo 13 descrive brevemente in quali contesti è conveniente impiegare
Mnesia, e introduce la configurazione e le operazioni classiche di scrittura, lettura, cancellazione,
indexing, svolte in contesto transazionale.
Nel
capitolo 14 si considera brevemente e sommariamente lo
sviluppo di interfacce grafiche con
wxErlang,
binding wxWidgets per il linguaggio Erlang. Qui, oltre a indicare i principi caratteristici del toolkit, sono sviluppati due piccoli esempi; comunque, per chi realmente fosse interessato all'argomento, si rimanda a ulteriori risorse.
La
programmazione distribuita in Erlang è semplice e potente, ma capita a volte di dover affidarsi a meccanismi più a basso livello. Pertanto, nel
capitolo 15,
Socket Programming, si mostra, anche qui senza entrare molto in profondità, come instaurare flussi di comunicazione
byte-oriented appoggiandosi a
UDP (User Datagram Protocol) e al protocollo
TCP (Transmission Control Protocol).
L'interfacciamento di Erlang con altri linguaggi di programmazione, e in particolare con
Java,
C, e
Ruby, trova spazio nel
capitolo 16. Questa interoperabilità è supportata o attraverso i port, i quali aprono un canale di trasmissione binario tra un nodo Erlang e un programma esterno, o attraverso primitive per la comunicazione con Erlang nel linguaggio da interfacciare, nel qual caso la gestione dei tipi è centrale. Ad esempio, la distribuzione Erlang contiene
JInterface, un package
Java con le classi corrispondenti ai tipi Erlang e le classi per l'interfacciamento, che sfruttano
EPMD (Erlang Port Mapper Daemon) per connettere il nodo
Java al nodo Erlang. Alternativamente, se è necessario minimizzare l'
overhead, una soluzione più sofisticata consiste nello sviluppare estensioni, affinché sia possibile eseguire programmi esterni all'interno dello stesso thread in cui è eseguito il runtime Erlang (
linked-in driver).
Una trattazione più completa e dettagliata è quella relativa al
debugging, argomento sviluppato nel
capitolo 17. In particolare, vengono introdotte due
BIF importanti:
erlang:trace/3 per abilitare o disabilitare il meccanismo di
tracing del runtime, ed
erlang:trace_pattern/3 per tracciare le chiamate locali e globali a funzione. Successivamente si descrive
dbg tracer, un'interfaccia più user-friendly verso le suddette
BIF. Infine, si illustrano le
match specification, generabili comodamente attraverso
dbg:fun2ms/1, e utilizzabili per regolare il
tracing.
Il
capitolo 18,
Types and Documentation, riprende i tipi di dato in Erlang, introducendo due direttive,
-type e
-spec, per associare sinonimi ai tipi e per specificare un prototipo per una funzione. Le annotazioni
-spec possono essere poi controllate con
TypEr, un tool in grado anche di generarle tramite un motore di inferenza. La specifica del tipo è utile per fini di documentazione, per cui è presente
EDoc, strumento capace di generare la documentazione relativa a uno o più file sorgenti, attingendo informazioni direttamente dalle annotazioni e dai commenti – il cui formato segue le regole del framework – presenti in essi.
Un
capitolo apprezzabile, sebbene non entri affatto in profondità, è senz'altro il
diciannovesimo,
EUnit and Test-Driven Development. Oltre ai vantaggi ormai noti dello
unit testing, sono esposti i lineamenti caratteristici del framework
EUnit. Se per le funzioni pure è sufficiente applicare asserzioni – implementate come macro – in previsione del loro output, per codice con
side-effects in genere occorre effettuare il
setup/cleanup dei dati (
fixture) ed impiegare oggetti
mock o
stub.
Il libro conclude in grande con il
capitolo 20,
Style and Efficiency, un distillato di esperienza con
consigli, best practices ed errori comuni da evitare, da utilizzare come linee guida per raggiungere una buona qualità interna nel software Erlang e per ricercare, ma solo successivamente, l'efficienza.