Abstract
Haskell è un linguaggio di programmazione complesso e flessibile che offre
soluzione a problemi piu'difficili da aggirare sfruttando gli strumenti offerti
da altri diffusi linguaggi, pur consentendo al programmatore di continuare lo
stesso a sfruttarne le tecniche. Potenza e praticità a un buon compromesso.
Data di stesura: 19/03/2006
Data di pubblicazione:
22/03/2006
Ultima modifica: 04/04/2006
Haskell è un linguaggio di programmazione ideato nel 1987. Creato con l'intento
esplicito di creare un linguaggio orientato alla programmazione funzionale,
con un design concepito a questo scopo, è il punto di sintesi delle esperienze
accumulate in questo campo dalla comunita'informatica nei decenni
precedenti. Nonostante continui ancor oggi ad evolversi molto rapidamente, forte
del rapido e costante flusso di innovazione a cui un numero sempre maggiore di
ricercatori contribuisce, esso gode di una definizione standard ben condivisa ,
che rende possibile utilizzarlo, apprenderlo od insegnarlo in una sua forma
omogenea, agevolando la convertibilita'del codice da un implementazione
all'altra e quindi il suo riuso. Quest'ultima, detta Haskell98, sarà anche la
base per future trasformazioni o miglioramenti del linguaggio.
Struttura
Lavorare con Haskell richiede l'adozione di nuove concezioni progettuali
rispetto a quelle tradizionalmente offerte dai tradizionali linguaggi di
programmazione param-oriented od object-oriented. Il calcolo immediato di
procedure lascia il posto a una tecnica di computazione raffinata, in cui sono
i risultati di precedenti operazioni a costruire i successivi, secondo una
procedura completamente definibile passo dopo passo : le espressioni possono
essere valutate solo quando lo si richiede (lazy evaluation) , o in un momento
precisamente definibile, oppure non esserlo affatto se lo si decide. Questo tipo
di logica puo'essere usata per gestire con naturalezza ed eleganza un numero
elevatissimo di casi che normalmente richiedono di saper gestire calcoli di
elevata complessità. In ambito logico-matematico è uno dei linguaggi più
adottati grazie alla sua agilità nel confrontare e combinare risultati di
equazioni.
Si tratta, quindi, di apprendere un intero stile di programmazione dai
rudimenti, e fare la conoscenza con procedimenti di abbreviazione veramente
molto utili. Però, questo non vuol significare "facili". Quindi, prima
di iniziare è bene conoscere bene sin da principio i principali operatori.
Operatori Aritmetici
Somma: +, -
Moltiplicazione, Divisione e Potenza: *, /, ^
Maggiore e minore di: >, <
Maggiore e uguale di, Minore e uguale di: >=, <=
Uguale e Non-Uguale: ==, /=
Operatori Logici
Logical And: &&
Logical Or: ||
Operatori su Liste
Concatenzione: ++
Differenza di Liste: \\
Operatore di Indice: !!
Range o Sequenza: ..
List Comprehension: <-
Operatori Funzionali
Lambda: \
Composizione di Funzioni: .
Operarore di Naming: =
Operatore di Type-mapping: ->
Operatore di Type-definition: ::
Operatore di Ereditarieta': =>
Binding: >>
Operatori di Pattern Matching (vedi oltre)
@ = ReadAs
! = Strict Evaluation
Trattandosi di un linguaggio puramente funzionale, Haskell offre un'imbattibile
elasticità nell'elaborazione di funzioni. Ben lungi dall'essere semplicemente
delle unita'per il riuso del codice, le funzioni sono insieme la base
costitutiva e la punta di diamante di Haskell:base costitutiva perché l'intero
ambiente del linguaggio beneficia dell'elasticità connaturata a questo tipo di
design, il che fornisce al coder la possibilità di definire operatori e tipi
in maniera del tutto libera, fino a poter creare interi nuovi linguaggi
derivati del linguaggio standard semplicemente definendoli a partire da
quest'ultimo. Non solo riuso di codice già scritto, dunque, ma possibilità
indefinite di espandere e riadattare ciò che si è già progettato.
Non sorprende, dunque, la presenza di costrutti adatti a svolgere meccanismi
automatici volti al riutilizzo di funzioni, come la ricorsione e la notazione
lambda. Com'e`noto a chi ha scritto almeno una funzione ricorsiva in qualsiasi
linguaggio, in assenza di meccanismi appositamente creati basta una svista per
produrre una recursione infinita, cosa spiacevole che principalmente nei
linguaggi procedurali si suole scongiurare utilizzando le condizioni di stop
nei cicli Haskell, però, possiede un costrutto dedicato a effettuare o meno le
operazioni prescritte in base alla veridicità di una o più condizioni;stiamo
parlando delle guards. Il loro inserimento all'interno del codice è anche un
raffinato modo per organizzare cicli con numerose condizioni, pur senza
rinunciare a mantenere nel codice una certa leggibilità.
Esempio:
| x = 0 equivale a if (x == 0) {...}
| z < y ...
Le funzioni di tipo lambda-object sono il costrutto che consente il più
immediato impiego di unità di codice funzionale: definite in modo anonimo,
possono essere richiamate in qualsiasi punto del flusso del codice per
computare al volo il risultato di un'operazione o della valutazione di
un'espressione;utilizzate con la lazy evaluation nel modo piu'opportuno possono
quindi diventare lo strumento ideale per predisporre raffinati meccanismi.
Hugs> map (\x -> 3*x + x/4) [1]
[3.25]
Come logica conseguenza della lazy-evaluation, in una funzione è perfettamente
possibile aggregare piu'valori per portarli a produrre un unico
risultato. Questo procedimento, detto currying, consente di accelerare
enormemente la scrittura di funzioni che estrapolino un output di un certo tipo
partendo da piu'valori di tipi differenti.
Perché "valori" e non "variabili"?Qualcuno di voi se lo
sarà già chiesto.
Semplicemente perché in Haskell non esistono "variabili" nel senso
classico di questo termine;bensì è meglio parlare di valori, intesi come
stati computazionali che una funzione può assumere mentre o dopo che le si
assegnano istruzioni. Quindi è preferibile evitare ambiguità, soprattutto per
chi proviene da esperienze di programmazione procedurale. Da notare che in caso
di bisogno di variabili si possono definire esplicitamente valori arbitrari:
Haskell dispone di modi molto pratici per assegnare comportamenti predefiniti a
insiemi di oggetti, o liste.
Ad esempio, le sequenze aritmetiche si possono schematizzare nella forma
[indice di partenza..indice di destinazione], ed anche la possibilità di
compiere serie di operazioni su una lista e assegnarle infine a un valore
esiste ed è offerta dal concetto di list comprehension.
Hugs> [ (x,y) | x <- [0..6], even x, y <- "ab" ]
[(0,'a'),(0,'b'),(2,'a'),(2,'b'),(4,'a'),(4,'b'),(6,'a'),(6,'b')]
Le proprietà delle list comprehension conducono ad un discorso che ha influito
pesantemente nello sviluppo, nell'evoluzione e nella preferibilità d'uso di
Haskell, inerente alla abstraction o astrazione. In termini pratici, in Haskell
è consentito esprimere i concetti in forma assoluta per poi poterli utilizzare
così come sono stati espressi, con una semplice parametrizzazione.
La convenienza della parametrizzazione si evidenzia in special modo nel pattern
matching. Normalmente, nei principali linguaggi di programmazione oggi diffusi
lo si trova implementato, ed usato per riconoscere la presenza di serie di
caratteri all'interno di altre serie. In Haskell lo si può introdurre nelle
espressioni per confrontarle tra loro analizzandone i parametri. Ne è un
esempio la dichiarazione della funzione map vista all'opera poc'anzi:
map :: (a->b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
La funzione map definisce un tipo map che, dati valori a e b, risulta in una
lista di valori b valutati a partire dai valori a. Quindi, essa definisce
automaticamente un istanza del tipo map, che e`appunto f, che inizialmente è
una lista vuota;poi, nel terzo e ultimo passaggio, si esegue un operazione di
pattern matching su f, che dà come risultato finale un espressione in cui il
valore x di f viene valutato secondo il valore xs.
Intere serie di valori possono essere manipolate all'interno di singole
espressioni, e con ulterior rapidita'usando wildcards come *, ? e simili.
Nel caso, ad esempio, che si utilizzi questa potente scorciatoia, è buona
pratica commentare accuratamente il proprio codice, il che è cruciale. Non è
vero che Haskell sia un linguaggio per superdotati o poco perspicuo, ma tanta
potenza richiede altrettanta chiarezza. Qualsiasi espressione compresa tra le
coppie sintattiche {- e -} viene interpretata come commento. È bene fare
attenzione a non confondere le parentesi graffe con [] o (), il primo crea
infatti una lista vuota, il secondo può definire una tupla, altro tipo
sequenza , oppure altri significati a seconda di dove lo si impiega. Gli errori
sintattici, se passano inosservati, possono introdurre a loro volta errori
logici di più difficile individuazione, in Haskell più che altrove!
Quindi, non stiamo trattando solo un linguaggio orientato alla funzionalità,
ma anche molto preciso nel definire le caratteristiche di oggetti in modo
astratto, usarle e derivarle.
Non bisogna però confondere questa modalità operativa propria di Haskell con
quella dei linguaggi di programmazione Object-Oriented, con cui apparentemente
esso sembra condividere questa caratteristica. Innanzitutto, Haskell non dispone
di tipi primitivi, e i tipi sono entità che è possibile di volta in volta
definire. I tipi sono resi disponibili dall'implementazione, e variano da una
all'altra, ma in buona sostanza alcuni di essi sono implementati praticamente
in modo standard. Esiste perciò un set di tipi minimo che è necessario
ricordare:
Bool valori di tipo vero/falso
Char valori di tipo carattere
Int valori numerici limitati a 32 bit
Integer valori numerici teoricamente illimitati
Float, Double valori numerici approssimati a precisione singola o doppia
Rational valori indicanti numeri razionali
La definizione delle classi è similare a quella già citata per i tipi,
ricordando però che queste non sono insiemi di proprietà e metodi e campi di
istanza come in maniera piu'o meno ovvia si presumerebbe al primo
impatto.
Esempio:
class Test a where
(-) :: a -> a -> Bool
Abbiamo visto la capacità di Haskell di definire nuovi tipi e classi
caratterizzate da essi, semplicemente a partire da valori astratti. L'astrazione
è però un'aspetto molto più vasto della programmazione funzionale, che
richiede tempo ed attenzione per essere ben padroneggiato.
Sfruttare l'alta flessibilità di Haskell impone la necessita'di padroneggiare
un costrutto specifico di questo linguaggio, cioè il costruttore di tipo o
monad (ital: Monade).
Le monadi sono costrutti peculiari di Haskell, al tempo stesso
fondamentali;quindi, così come nella programmazione OO con i concetti di
ereditarietà, incapsulamento e polimorfismo sono alla base della logica
applicativa che agisce sulle scelte di design, la capacità di strutturare
applicazioni in Haskell non può prescindere dall'apprendimento delle loro tre
proprietà fondamentali:
Modularità
Esse distinguono ogni singolo valore coinvolto in un certo calcolo, in
maniera del tutto indipendente da cosa si ottiene valutandolo
nell'espressione in cui compare;dunque per tale ragione possono suddividere i
calcoli in segmenti e modificare la strategia di calcolo senza con ciò
interferire con l'operazione in corso.
Flessibilità
Le monadi consentono di creare una strategia di calcolo specifica per ognuna
di esse, quindi di assumere un controllo differenziato delle procedure di
calcolo per ciascuna parte dei programmi. Più il programma assumerà un
design modulare, più di conseguenza si adatterà ad essere modificato,
raffinato ed ampliato passo dopo passo.
Isolamento
Ogni monade può agire più o meno di concerto con le altre, e la
possibilità di eseguire al loro interno calcoli separati dal resto del
programma consente di ricorrere a qualunque tipo di operazione, anche non
propriamente consona al coding style dell'applicazione, senza contaminare la
logica funzionale del programma nel suo complesso.
È bene abituarsi a non confondere i concetti di classe e monade, bensì
abituarsi a considerare le prime come insiemi di proprietà e di metodi, le
altre come centri di combinazione di calcoli;che le prime definiscono le
caratteristiche di oggetti, le seconde formano punti di riferimento a cui altre
parti del programma accederanno o meno per collaborare nelle computazioni.
Tenere presente se si vuole la regola pratica che, come l'unità di base della
programmazione ad oggetti è o'a classe , la monade e`l'unità di base
corrispondente di un programma funzionale.
Il polimorfismo è però certamente utilizzato in Haskell, anche se in modo
differente nelle molteplici implementazioni e di norma viene applicato
introducendo nel codice la keyword "data", facendola seguire dai nomi
dei tipi previsti separandoli con un pipe(|). Tracciando un discorso
riassuntivo, in Haskell lo si utilizza prevalentemente per definire tipi di
funzioni e di classi, oppure gruppi di funzioni che agiscono su tipi di
oggetti, caratterizzandole con specifici comportamenti per farlo. Però allo
stesso tempo, le monadi sono organizzate secondo una logica ad oggetti. Le
monadi derivano una superclasse, Monad, di cui le stesse sono definite come
istanze. Essa è così definita:
infixl 1 >>, >>=
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
m >> k = m >>= \_ -> k
Va comunque detto che Haskell lascia allo sviluppatore un'enorme libertà nello
strutturare i propri programmi. Si potrà scegliere uno stile più orientato
verso la filosofia ad oggetti, così come uno funzionale. Nel secondo caso,
scegliere se utilizzare solo monadi primitive oppure monadi create
combinandole, o non adoperarle affatto, anche se questo non farà altro che
costituire un limite. La monade Maybe, che si comporta da type constructor, e la
monade IO, tanto particolare da meritare un'apprendimento dedicato, sono punti
di partenza utili ed essenziali.
Module Main where
myLine :: String -> IO (maybe String)
[...]
Sintatticamente, una monade è rappresentata da un type constructor, seguita
dalla dichiarazione di una funzione che restituisce valori di quel tipo, e da
una procedura di calcolo specifica per ottenere oggetti del medesimo. Questi due
ultimi elementi prendono i nomi di return e bind, e sono definiti come metodi
della superclasse Monad precedentemente esaminata. Quindi, ricordando che tutte
le monadi derivano Monad, essi sono a disposizione ogniqualvolta se ne
definisce una nuova.
Giunti a questo punto, per semplificare e snellire il lavoro con le monadi,
saranno utili cenni sulla notazione "do". Quest'ultimo è uno stile
sintattico usato in Haskell per emulare lo stile di programmazione
procedurale. E strutturare cicli in modo tradizionale è molto più semplice
così facendo che usando le corrispondenti istruzioni in stile funzionale.
Module Main where
myLine :: String -> IO (maybe String)
myLine alllines =
do contents <- readFile alllines
let n = read contents
writeFile alllines (show (n+1))
return n
In GHC (vedi sotto) è disponibile anche la mdo-notation, detta anche notazione
do ricorsiva. Chi fosse interessato troverà raggugagli nella documentazione di
questo software.
IO è una monade fondamentale da conoscere, non solo perché la sua azione main
è responsabile dell'avvio dei programmi scritti in Haskell, quanto perché
solitamente è l'unica a essere utilizzabile in modo diretto e quasi sempre
esplicito pur rimanendo semplice da sperimentare. Si precisa che un programma in
Haskell è la funzione main contenuta in un modulo Main, e che il suo contenuto
non viene eseguito immediatamente, al contrario ognuna delle istruzioni
descritte verrà eseguita al momento opportuno;in altre parole l'istruzione
main è un'azione che ritorna una serie di istruzioni sotto forma di una
descrizione. Essa va a costituire un programma, ma sotto quest'aspetto è una
semplice azione IO, che ritorna una descrizione di una o più operazioni del
suo stesso tipo. Nel caso di azioni interattive, a cui questa monade è
specificamente indirizzata, le informazioni risultanti verranno indirizzate al
flusso di output, e l'interprete provvederà a gestirle stampando l'output
sullo schermo oppure, se errate, lanciando eventuali eccezioni.
Si noti che non abbiamo invocato una funzione che stampasse a schermo ciè che
si è digitato, ma abbiamo istanziato un'azione di tipo IO dandole la direttiva
di produrre un'azione che stampasse su schermo i dati che abbiamo
introdotto. Partendo dall'immissione dei dati, abbiamo introdotto un valore,
tradotto quest'ultimo in un'azione IO, che senza fare altro ritorna
semplicemente il valore. Il valore diventa l'agente di se`stesso e si riproduce
come azione. Bisogna abituarsi a intendere diversamente i valori a seconda dei
loro ruoli, che variano da esigenza a esigenza.
Conclusioni
È importante scegliere sin da subito l'implementazione che si utilizzerà per
produrre il proprio codice, poiché comportano curve di apprendimento molto
differenti. È da ricordare che il linguaggio è stato molte volte revisionato,
esteso ed arricchito andando così inevitabilmente modificandosi nel corso
degli anni. Quindi, idee differenti sono entrate a far parte di concezioni
differenti e iò si riflette ampiamente nella diversità delle implementazioni
disponibili.
Hugs è la più valida alternativa a GHC. Supporta sia lo standard Hugs che
Haskell98, ed è anch'esso disponibile su più piattaforme. Inoltre è molto
ricco di funzionalità aggiuntive, come una gran quantità di tipi,
funzionalità shell-like, presenza di molti moduli avanzati e sperimentali. Per
la sua immediatezza di installazione e utilizzo è il più raccomandabile per
iniziare, pur rimanendo una validissima implementazione quasi completa e
certamente affidabile. In ogni caso è consigliabile sfogliare spesso la
documentazione per rilevare eventuali discrepanze che possano ostacolare
l'apprendimento prima e poi lo sviluppo di software.
ATTENZIONE: Hugs non permette di definire nuovi tipi nell'interprete, ed esegue
applicazioni complete solamente leggendole da file;quindi, il primo passo per
provare un codice è salvarlo su di un file con estensione .hs, con un modulo
main definito al suo interno, e successivamente caricarlo;questo si esegue per
mezzo dell'istruzione :load, che prende come argomento il percorso del file che
si intende importare.
Ad esempio, scrivendo nell'interprete:
:load "Esempio.hs"
Caricherete nell'interpete il modulo Haskell contenuto nel file Esempio.hs.
È bene notare che anche semplicemente definire una classe richiede il
salvataggio su file del codice. Provando a scrivere una semplice definizione di
classe prima nell'interprete e poi in un file successivamente caricato,
otteniamo due differenti risultati, il primo dei quali è un messaggio di
errore:
Hugs> class myDef x where :: a -> a -> String
ERROR - Syntax error in expression (unexpected keyword "class")
Hugs> :load "myDef.hs"
Main>
Bibliografia
La programmazione in Haskell è un argomento ampio e complesso, a cui questo
tutorial voleva solamente essere un invito all'approfondimento. Soltanto
praticando molte soluzioni e stili differenti si potrà avere una chiara idea
delle potenzialità espressive praticamrnte illimitate di questo linguaggio. A
tale scopo si consiglia a chi fosse interessato a proseguire l'apprendimento,
di leggere almeno tre di questi testi:
Per saper utilizzare al meglio le monadi, esistono altri testi esplicitamente
dedicati alla loro trattazione.È bene conoscerli, visto che alcuni testi
generici non trattano affatto questa parte.Ecco alcune letture interessanti: