Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / Haskell

Introduzione ad Haskell

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
di Ivan Furone Discuti sul forum [1]   Stampa

Cenni Storici

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:

Informazioni sull'autore

Ivan Furone, studente universitario di Lingue e Linguistica presso l'Università degli Studi di Roma Tre.

È possibile consultare l'elenco degli articoli scritti da Ivan Furone.

Altri articoli sul tema Linguaggi / Haskell.

Discuti sul forum [1]   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.

Altri articoli