Secondo il sondaggio 2024 di Stack Overflow, Rust è il linguaggio più desiderato dai programmatori di tutto il mondo, quello che tutti vorrebbero usare per sviluppare le applicazioni.
Rust, però, è anche classificato come un linguaggio a basso livello, un “C fatto bene”. Ha senso utilizzarlo per scrivere applicazioni web?
Rust oltre i pregiudizi
Da qualche mese ho iniziato a studiare Rust. Il sondaggio di SO e una sessione al DDD open su un caso di utilizzo di Rust in un’applicazione event sourcing hanno riavviato una mia latente curiosità per Rust.
Rust non è nuovo per CodicePlastico, ne avevamo già parlato qualche anno fa in questo articolo L’ownership in Rust, spiegata con le torte – PlasticBlog. Io, personalmente, avevo letto qualche articolo introduttivo e l’avevo classificato come linguaggio inutilmente complicato utile solo per scrivere driver.
Giudizio affrettato, mea culpa.
Con un po’ più di saggezza e di conoscenza dei linguaggi di programmazione ho ricominciato da zero leggendo il libro di riferimento e facendo qualche classico kata per abituarmi ai tool e alla sintassi.
Ma andiamo con ordine…
Rust nasce intorno al 2010, prima come progetto personale di Graydon Hoare, poi, grazie a Mozilla, diventa un progetto finanziato e sostenibile e nel 2015 raggiunge la prima versione stabile. Ha una sintassi simile al C, non segue un particolare paradigma, abbraccia alcuni principi della programmazione funzionale e della programmazione ad oggetti tramite l’uso delle struct (alle quali si possono aggiungere dei metodi), ma il suo approccio è principalmente imperativo. E’ fortemente tipizzato, supporta i generics e ha i traits.
I suoi punti di forza, usati anche come selling point, sono le performance (zero-cost-abstraction), la type safety e la gestione corretta della concorrenza.
Come C e C++ non ha il garbage collector e, anche se la gestione della memoria è demandata al programmatore, dispone di un Borrow Checker che a compile time è in grado di capire se le risorse sono state gestite correttamente riducendo al minimo i possibili errori. Non ha eccezioni e non ha il null.
E proprio il borrow checker è la croce e delizia dei neofiti che si avvicinano a Rust. Croce perché non gli sfugge niente e all’inizio è una lotta continua con il compilatore e tentativi di usare &, *, &mut e altri strani simboli che Rust mette a disposizione per far si che il tuo programma compili, ma, da un altro punto di vista, e soprattutto una volta capito come funziona, è una gran sicurezza avere un compagno che ti aiuta a verificare che il tuo codice non crei memory leak e che i riferimenti e le risorse siano usate in modo corretto.
Proprio questa peculiarità del linguaggio unita al fatto che sia generalmente definito come un system language ha frenato il mio entusiasmo qualche anno fa.
Ma, come dicevo sopra, da qualche mese mi sono riavvicinato con un occhio diverso, ho studiato e capito i temi del borrowing e del move (quasi 😛) e ho scoperto altre feature molto interessanti di Rust: il type system e il suo lato funzionale.
Una cosa che mi ha sorpreso è l’assenza del null! Le strutture e i valori devono sempre avere un valore e il concetto tanto odiato di null e tutto quello che si porta dietro (NullReferenceException e amici) scompare.
Rust rinuncia al null grazie a due meccanismi garantiti dal compilatore. Prima di tutto, quando dichiaro una variabile, sono costretto ad assegnarle un valore. Quindi non posso scrivere:
let x: i32;
Ma devo scrivere:
let x: i32 = 42;
Questa peculiarità unita al concetto di Option, preso dai linguaggi funzionali permette di creare tipi che possono o meno avere un valore.
pub enum Option<T> {
None,
Some(T),
}
Una funzione che effettua una serie di calcoli e non può garantire di tornare sempre un valore, può tornare un Option<i32> ( o un Result<T, Err> ).
Ma il vero punto di forza di Rust è, secondo me, il type system: supporta sia i sum type che i product type e ha i trait per gestire il polimorfismo e i generics.
Rust è anche il linguaggio di riferimento per WebAssembly, si può scrivere codice Rust, compilarlo in .wasm ed eseguirlo nel browser con performance molto vicine al nativo.
Un po’ di codice
Il modo migliore per imparare e capire un linguaggio di programmazione è usarlo per realizzare qualcosa. Con Luca abbiamo scritto una semplice versione del vecchio gioco Snake. Trovate i sorgenti qui e potete giocare online qui.
Il gioco utilizza una sola libreria esterna alla core library di Rust per gestire gli aspetti prettamente legati alla grafica e all’IO: abbiamo optato per Macroquad per la sua semplicità.
L’applicazione è composta da diversi moduli che implementano il comportamento dei vari moduli.
Siamo partiti definendo una struttura Snake e implementando la logica di controllo del serpente. Ha una posizione rappresentata dalla sua testa, una coda, una direzione e uno stato.
pub struct Snake {
pub head: Position,
pub tail: Vec<Position>,
pub direction: Direction,
pub state: SnakeStates,
}
Come vedete già in questo primo esempio abbiamo tipizzato bene tutti gli attributi della struttura Snake usando il dominio del gioco:
pub struct Position(pub f32, pub f32);
pub enum Direction {
North,
East,
South,
West,
}
pub enum SnakeStates {
Alive,
SelfEaten,
Smashed,
}
Position, in particolare, è una struct rappresentata tramite una tupla di due elementi: le due coordinate x e y.
La struttura Snake ha una serie di metodi che ne gestiscono il comportamento: movimento, crescita, cambio di direzione e controllo dello stato. Ad esempio, la funzione che fa crescere il serpente è implementata cosi:
pub fn grow(&mut self) {
self.len += 1;
let last = self.tail.last();
match last {
Some(l) => self.tail.push(l.clone()),
None => self.tail.push(self.head.clone()),
}
}
Abbiamo implementato anche la struttura Food che rappresenta la posizione del croccantino che viene mangiato dal serpente e la struttura App che rappresenta l’applicazione e implementa i metodi update e render gestiti dal main loop dell’applicazione: update controlla se il serpente si trova sul croccantino o se è contro un muro, render si occupa invece di disegnare il serpente, il croccantino e e i muri.
Se date un’occhiata ai sorgenti vi renderete conto di quanto sia semplice capire l’implementazione e di come la separazione in strutture renda tutto più modulare e facilmente gestibile.
Il compilatore verifica la compatibilità dei tipi, il ciclo di vita degli oggetti e la validità delle reference utilizzate.
La compilazione tramite il comando cargo build --target wasm32-unknown-unknown genera il file wasm che viene poi referenziato dalla pagina index.html e questo basta per fruire il gioco da un browser. Se invece preferite usarlo come eseguibile sul vostro laptop va compilato con il comando cargo build.
Implementare snake è stato un esercizio divertente per imparare alcuni concetti di Rust non prettamente legati a semplici kata e anche per usare una libreria di grafica 2D come macroquad e per capire come deployare via web un’applicazione Rust.
CodicePlastico si butterà su Rust?
E’ ancora presto per decidere, lo stiamo studiando, abbiamo un gruppo interno di Rustaceans che una volta alla settimana si dedica allo studio di Rust (…in queste settimane stiamo provando a superare le challenge del sito Protohackers). Stiamo anche provando a scrivere qualche API per valutarlo come general purpose language. Al momento ci sta dando soddisfazione, ma è presto per dire se diventerà uno dei linguaggi aziendali.