Cos'è Haskell e perché impararlo anche se non lo userai
Cos'è Haskell: funzioni pure imposte dal compilatore, valutazione pigra, tipi potentissimi e perché le sue idee sono ormai in Rust, Swift e TypeScript.
Usi map e filter ogni giorno, controlli se un valore è null per la centesima volta, e intanto qualcuno ti dice che "dovresti provare Haskell" con l'aria di chi ha visto la luce. Ti chiedi se valga il tempo, visto che nessun annuncio di lavoro lo chiede. In questo articolo trovi cos'è Haskell, cosa lo rende così diverso, e perché ha senso studiarlo anche se non lo userai mai in produzione.
Cos'è Haskell
Haskell è un linguaggio di programmazione funzionale puro, a tipizzazione statica e con valutazione pigra: le funzioni non possono avere effetti collaterali, e non è una buona pratica consigliata ma una regola che il compilatore fa rispettare.
Nasce nel 1990 da un comitato di ricercatori che voleva un linguaggio funzionale comune, invece della dozzina di linguaggi sperimentali sparsi tra le università. Il nome viene da Haskell Curry, logico matematico. Il compilatore di riferimento oggi è GHC.
Se il paradigma funzionale ti è nuovo, le basi — funzioni come valori, immutabilità, composizione — sono spiegate in paradigmi di programmazione. Qui vediamo cosa succede quando quel paradigma viene portato fino in fondo, senza compromessi.
Funzioni pure: una regola, non un consiglio
Una funzione pura ha due proprietà: dato lo stesso input restituisce sempre lo stesso output, e non modifica niente fuori da sé. Niente variabili globali cambiate, niente scritture su file, niente stampe a video.
In JavaScript o in Python puoi scrivere funzioni pure, ed è una buona idea. Ma nessuno ti impedisce di infilarci un console.log o di modificare un oggetto ricevuto come parametro. In Haskell non puoi: se una funzione dichiara di prendere un intero e restituire un intero, il compilatore garantisce che non fa nient'altro.
quadrato :: Int -> Int
quadrato x = x * x
sommaQuadrati :: [Int] -> Int
sommaQuadrati xs = sum (map quadrato xs)
La riga con :: è la firma: quadrato prende un Int e restituisce un Int. Punto. Nessuna sorpresa nascosta.
Perché è utile? Perché una funzione pura si capisce guardando solo lei. Non devi sapere in che ordine viene chiamata, cosa è successo prima, quale stato globale c'è in giro. Si testa passando un input e controllando l'output. E si può eseguire in parallelo senza paura, perché non tocca niente di condiviso.
La valutazione pigra
Il secondo tratto distintivo è la lazy evaluation: in Haskell niente viene calcolato finché il risultato non serve davvero.
La conseguenza più spettacolare è che puoi definire strutture infinite:
naturali :: [Integer]
naturali = [1..]
primiCinquePari :: [Integer]
primiCinquePari = take 5 (filter even naturali)
-- [2,4,6,8,10]
naturali è la lista di tutti i numeri naturali. Non esplode perché Haskell calcola solo gli elementi che take 5 richiede. Questo permette di separare la descrizione di "cosa" produrre dalla decisione di "quanto" produrne, in modo molto elegante.
Il rovescio della medaglia è serio. Se niente viene calcolato subito, Haskell accumula in memoria "promesse di calcolo" (si chiamano thunk) finché qualcuno non le richiede. In certi casi quelle promesse si accumulano a milioni e il programma consuma memoria in modo imprevedibile. Ragionare su tempi e memoria in Haskell è più difficile che in un linguaggio che esegue le istruzioni nell'ordine in cui le scrivi, ed è una delle critiche più fondate che gli vengono fatte.
Il sistema di tipi e il "se compila, funziona"
Haskell ha uno dei sistemi di tipi più espressivi tra i linguaggi usati fuori dalla ricerca, e allo stesso tempo ti chiede di scriverne pochi, grazie all'inferenza: il compilatore deduce da solo i tipi dal modo in cui usi i valori.
Lo strumento più influente sono i tipi algebrici: descrivi un dato come "una di queste alternative", e il compilatore ti obbliga a gestirle tutte.
data Forma
= Cerchio Double
| Rettangolo Double Double
| Triangolo Double Double
area :: Forma -> Double
area (Cerchio r) = pi * r * r
area (Rettangolo b h) = b * h
area (Triangolo b h) = b * h / 2
Se domani aggiungi Esagono e dimentichi di aggiornare area, il compilatore (con gli avvisi attivi) te lo segnala. In un linguaggio senza questa garanzia, lo scopri a runtime.
Da qui viene la frase che senti spesso: "se compila, funziona". Va presa con cautela. È vero che un'intera categoria di errori — tipi sbagliati, casi dimenticati, null inattesi — sparisce prima dell'esecuzione. Ma il compilatore non sa se la tua logica è corretta: se scrivi b * h dove serviva b + h, compila benissimo. La frase giusta sarebbe: se compila, hai eliminato molti errori banali e puoi concentrarti su quelli veri.
Niente null: Maybe
In Haskell il valore null non esiste. Se una funzione può non trovare un risultato, lo dice nel tipo:
cercaUtente :: Int -> Maybe String
cercaUtente 1 = Just "Giulia"
cercaUtente _ = Nothing
saluto :: Int -> String
saluto n = case cercaUtente n of
Just nome -> "Ciao " ++ nome
Nothing -> "Utente sconosciuto"
Non puoi usare il risultato come se fosse una stringa: devi prima gestire il caso Nothing. L'errore più comune del software — accedere a qualcosa che non c'è — diventa impossibile per costruzione.
Gli effetti gestiti con i tipi (e le monadi)
A questo punto sorge la domanda ovvia: se le funzioni non possono avere effetti collaterali, come si legge un file o si stampa qualcosa? Un programma che non comunica col mondo non serve a niente.
La soluzione di Haskell è mettere gli effetti nel tipo. Una funzione che fa input/output ha un tipo diverso, IO, e il compilatore tiene separato il codice puro da quello che interagisce con l'esterno:
main :: IO ()
main = do
putStrLn "Come ti chiami?"
nome <- getLine
putStrLn (saluto' nome)
saluto' :: String -> String
saluto' nome = "Ciao, " ++ nome
saluto' è pura; main è marcata IO. Guardando le firme sai subito quali parti del programma possono toccare il mondo esterno e quali no. Una funzione pura non può chiamarne una IO: il compilatore lo impedisce.
Il meccanismo generale che rende possibile concatenare operazioni di questo tipo si chiama monade. Non serve capirle adesso, e questo non è il posto per il tutorial. Basta sapere il problema che risolvono: permettono di descrivere sequenze di operazioni con un "contesto" — che può fallire (Maybe), fare I/O (IO), produrre più risultati (liste) — senza rinunciare alla purezza. Quando in JavaScript concateni .then() su una Promise, stai usando un'idea molto vicina.
Perché impararlo se nessuno ti pagherà
Qui serve dirlo senza giri di parole: in Italia quasi nessuno ti assumerà per scrivere Haskell. Esiste nel mondo in nicchie precise — finanza quantitativa, blockchain, strumenti di analisi del codice, qualche azienda che ci ha scommesso — ma le offerte sono rare e quasi sempre all'estero o da remoto.
Il suo valore è formativo. Le idee nate o maturate in Haskell sono uscite dal laboratorio e sono ovunque:
| Idea di Haskell | Dove la ritrovi |
|---|---|
map, filter, fold | JavaScript, Python, Java Streams, praticamente ovunque |
Maybe al posto di null | Option in Rust, optional in Swift e Kotlin |
| Tipi algebrici e pattern matching | enum in Rust e Swift, sealed class in Kotlin, union discriminate in TypeScript |
| Inferenza dei tipi | Rust, Kotlin, TypeScript, Swift |
| Immutabilità di default | Rust, e come buona pratica in quasi tutti gli ecosistemi moderni |
Quando impari Haskell, queste cose smettono di essere funzioni che usi per abitudine e diventano un modo di pensare. Torni al tuo linguaggio di tutti i giorni e cominci a separare la logica pura dagli effetti, a modellare i dati in modo che gli stati impossibili non si possano rappresentare, a diffidare dei null. Il codice che scrivi dopo è diverso — ed è questo il motivo per cui vale le settimane che ci metti.
Se invece cerchi un linguaggio funzionale da usare davvero in produzione, più pragmatico, guarda Elixir.
Errori comuni
Partire dalle monadi. Molti abbandonano perché il primo tutorial che trovano parte dalla teoria delle categorie. Comincia da funzioni, liste, tipi algebrici e pattern matching; le monadi arriveranno quando ne sentirai il bisogno.
Sottovalutare la pigrizia. Il caso classico è sommare una lista enorme con foldl: accumula milioni di thunk e consuma memoria inutilmente. La versione stretta foldl' risolve. È il tipo di problema che in altri linguaggi non esiste.
Combattere il compilatore. All'inizio gli errori di tipo sembrano ostili e lunghissimi. In realtà ti stanno dicendo dove il tuo ragionamento non è coerente: leggerli con calma è metà dell'apprendimento.
Sceglierlo come primo linguaggio per trovare lavoro. È un ottimo secondo o terzo linguaggio, non una scorciatoia verso l'assunzione. Se parti da zero, vedi imparare a programmare da zero.
In sintesi
Haskell è un linguaggio funzionale puro: le funzioni non hanno effetti collaterali, e a garantirlo è il compilatore, non la disciplina di chi scrive.
La valutazione pigra è insieme una forza e un limite. Permette liste infinite e codice molto espressivo, ma rende difficile prevedere l'uso della memoria.
Il sistema di tipi elimina intere categorie di errori: niente null, casi dimenticati segnalati, effetti visibili nella firma. "Se compila, funziona" è un'esagerazione utile, non una garanzia.
Il motivo per impararlo è formativo, non professionale. Poche aziende ti pagheranno per scriverlo, ma le sue idee sono già nei linguaggi che usi, e capirle alla fonte ti fa scrivere codice migliore ovunque.
Per vedere come si collocano Haskell e gli altri linguaggi nel panorama attuale c'è tutti i linguaggi di programmazione.