coldwa.st
Tutte le guideProgrammazioneWebDatiStrumentiDatabaseHaskellConcettiCabal e buildToolchainCompilatorePrestazioniEditor e HLS

Haskell · Rust · progettazione dei linguaggi

Haskell vs Rust: due risposte alla stessa domanda

Di ColdwastAggiornato il 31 luglio 20268 min di lettura#haskell#rust#types
Due sentieri sterrati che divergono attraverso un prato sotto un cielo coperto
Due sentieri sterrati che divergono attraverso un prato prima di ricongiungersi in lontananza, sotto un cielo coperto.

Haskell e Rust vengono confrontati di continuo, e il confronto è quasi sempre impostato come una gara. Questa impostazione nasconde ciò che è davvero interessante: entrambi i linguaggi partono dalla stessa premessa, cioè che il compilatore debba impedirti di scrivere programmi rotti, e poi prendono strade opposte sull’unica decisione che plasma tutto il resto.

Dove sono d’accordo

Entrambi caricano un peso insolito sul sistema di tipi. La descrizione che Haskell dà di sé è che «ogni espressione in Haskell ha un tipo che viene determinato a tempo di compilazione», e che il suo sistema di tipi forte «fa in modo che non ci siano sorprese». Il compilatore di Rust fa rispettare le proprie regole con la stessa intenzione, rifiutandosi di compilare codice che le violi.

In entrambi i casi lo scambio è lo stesso: dedichi più tempo a far compilare il programma e meno tempo a fare il debug di una classe di fallimenti a runtime. Se quello scambio non ti piace, nessuno dei due linguaggi ti sembrerà piacevole, e la scelta tra i due è fuori tema.

Dove divergono: la memoria

È la decisione da cui discende tutto il resto.

Il Rust Book la presenta come tre opzioni: «Alcuni linguaggi hanno una garbage collection che cerca regolarmente la memoria non più utilizzata mentre il programma è in esecuzione; in altri linguaggi il programmatore deve allocare e liberare esplicitamente la memoria. Rust usa un terzo approccio: la memoria è gestita tramite un sistema di ownership con un insieme di regole che il compilatore verifica.»

La memoria viene rilasciata in modo deterministico: «la memoria viene restituita automaticamente non appena la variabile che la possiede esce dal suo ambito», attraverso una funzione drop che il compilatore chiama alla parentesi graffa di chiusura. E il costo si paga a tempo di compilazione anziché a runtime, dato che «nessuna delle caratteristiche dell’ownership rallenterà il tuo programma mentre è in esecuzione».

Haskell prende la prima di queste tre opzioni. GHC «viene fornito con un garbage collector parallelo ad alte prestazioni e una libreria di concorrenza leggera». Non ragioni in termini di ownership perché è il runtime a occuparsi del recupero per te.

Questa singola differenza spiega gran parte del resto. Rust può rivolgersi a contesti in cui un garbage collector è inaccettabile, e in cambio ti chiede di soddisfare il borrow checker. Haskell toglie dalla tua giornata quell’intera categoria di lavoro, e ti chiede di accettare un runtime che si occupa della raccolta al posto tuo.

Dove divergono: la valutazione

La seconda differenza reale è il momento in cui le cose accadono.

Haskell è pigro per impostazione predefinita. Come dice haskell.org, «le funzioni non valutano i loro argomenti», e la conseguenza dichiarata è composizionale: «questo significa che i programmi si compongono molto bene tra loro». Puoi definire strutture concettualmente infinite e consumarne solo la parte che ti serve.

Rust valuta in modo stretto, come la maggior parte dei linguaggi. Un argomento viene calcolato prima della chiamata.

La pigrizia è davvero potente e davvero più difficile da valutare in termini di prestazioni, perché il momento in cui avviene un calcolo non è più dove l’hai scritto. La valutazione stretta è più prevedibile e meno espressiva proprio su questo punto. Nessuna delle due è un difetto; sono impostazioni predefinite diverse.

La purezza, e cosa porta con sé

Haskell descrive ogni funzione come «una funzione in senso matematico (cioè "pura")», ed elenca «trasparenza referenziale, immutabilità e valutazione pigra» tra le sue caratteristiche. Trasparenza referenziale significa che un’espressione può essere sostituita dal suo valore senza cambiare il programma, ed è ciò che rende sicuri ragionamenti e refactoring aggressivi.

Rust non è puro, e non pretende di esserlo. Ti dà invece mutazione controllata: il borrow checker regola chi può mutare cosa e quando, così la mutazione resta sicura senza essere vietata.

Allora, a quale domanda stai rispondendo?

Il modo utile di scegliere è chiedersi cosa il progetto non può tollerare.

Se non può tollerare un garbage collector, perché stai scrivendo un componente di sistema operativo, un target embedded, un motore di gioco o qualsiasi cosa con requisiti di latenza stringenti, la questione è decisa. Rust esiste per quel vincolo.

Se non può tollerare una logica scorretta, e il dominio è abbastanza complesso da rendere il ragionamento stesso la parte difficile, la purezza e il sistema di tipi di Haskell puntano esattamente lì. Compilatori, modellazione finanziaria e strumenti per linguaggi ricorrono negli usi reali di Haskell proprio per questo motivo.

Se nessuno dei due vincoli si impone, questo confronto probabilmente non è il tuo fattore decisivo. La disponibilità di librerie, le assunzioni e ciò che il tuo team già conosce conteranno più della semantica del linguaggio, ed è onesto dirlo.

La versione breve

Haskell e Rust concordano sul fatto che il compilatore debba intercettare i tuoi errori, poi rispondono a due domande diverse. Rust risponde a «come ottengo la sicurezza della memoria senza un garbage collector», con l’ownership verificata a tempo di compilazione e senza costi a runtime. Haskell risponde a «come rendo difficile sbagliare la logica stessa», con purezza, pigrizia e un garbage collector che si occupa del recupero.

Non sono davvero concorrenti. Sono due strade che partono dallo stesso punto, e il vincolo che non puoi negoziare ti dice su quale ti trovi.

Se il primo dei due è nuovo per te, comincia da che cos’è Haskell e, quando arrivi a costruire progetti, da Stack contro Cabal.

La descrizione della gestione della memoria e le citazioni provengono dal capitolo ufficiale del Rust Book sull’ownership; le descrizioni di purezza, pigrizia, sistema di tipi e garbage collector provengono da haskell.org. Entrambe sono state verificate al momento della stesura. Le implementazioni dei linguaggi evolvono, quindi verifica sulla documentazione attuale prima di fare affidamento su un’affermazione specifica.