Marionetta, astrazione II°

La logica del movimento di un singolo pezzo deve servire a modificare tutta la struttura, ma non abbiamo sviluppato lo strumento per farlo.

Un buon esercizio per imparare a percorrere l’albero è implementare la sua istanza di funtore, che significa applicare una funzione data a tutti i contenuti dei suoi nodi.

Cerchiamo una funzione

funtoreAlbero :: (a -> b) -> Albero a -> Albero b

Anche le liste sono funtori attraverso

map :: (a -> b) -> [a] -> [b]

Per il datatype Albero si tratta di smontare un nodo, applicare la funzione (a -> b) al suo contenuto di tipo ‘a’ e applicare la stessa funzione a tutti i suoi sottonodi.

funtoreAlbero f (Albero x ys) = Albero (f x) $ map (funtoreAlbero f) ys

Controlliamo i tipi coinvolti


*Main> :t funtoreAlbero
funtoreAlbero :: (a -> b) -> Albero a -> Albero b
*Main> :t \f -> map (funtoreAlbero f)
\f -> map (funtoreAlbero f) :: (a -> b) -> [Albero a] -> [Albero b]
*Main>

La funzione funtoreAlbero è corretta come tipo.
Il secondo tipo richiesto ci introduce al concetto di astrazione lambda, ovvero funzioni anonime. Sono funzioni create lì per lì, e che quindi non desideriamo riutilizzare altrove.
La sintassi utilizza la coppia ‘\’ e ‘->’ . Dopo il simbolo ‘\’ (lambda) si introducono gli argomenti, dopo il simbolo ‘->’ la definizione della funzione.
In realtà questo è l’unico modo di scrivere le funzioni.

funzione x y z = blah blah

si traduce in

funzione = \x y z -> blah blah

Tornando alla richiesta, abbiamo chiesto il tipo di una funzione con argomento ‘f’ definita come “map (funtoreAlbero f)”. Il risultato ci dice che ‘f’ è di tipo (a -> b) , l’argomento della funzione “map (funtoreAlbero f)” sarà una lista di ‘Alberi a’ e produrrà una lista di ‘Alberi b’. Quindi esattamente quello che cercavamo per trasformare i sottonodi.

Vediamo come funziona

*Main> let x = Albero 0 [Albero 1 [Albero 2 [],Albero 3 []],Albero 4 []]
*Main> funtoreAlbero (\x -> x * 2) x
Albero 0 [Albero 2 [Albero 4 [],Albero 6 []],Albero 8 []]
*Main> 

La funzione passata è una lambda che moltiplica per 2 il suo argomento. L’albero ottenuto ha la stessa struttura dell’albero originale e i contenuti dei suoi nodi moltiplicati per 2.

*Main> funtoreAlbero even x
Albero True [Albero False [Albero True [],Albero False []],Albero True []]
*Main> funtoreAlbero (\y -> even y) x
Albero True [Albero False [Albero True [],Albero False []],Albero True []]

Qui la funzione produce valore vero se il suo argomento è pari. Notare che scrivere even è come scrivere \y -> even y. Questo tipo di semplificazione si chiama riduzione eta.

Il funtore è molto limitato, perché applica la stessa funzione a tutti i nodi. Se volessimo applicare una funzione con parametri diversi in ogni nodo, non riusciremmo a farlo.
Allora costruiamo un’altra HOF: zipAlberoWith.

zipAlberoWith :: (a -> b -> c) -> Albero a -> Albero b -> Albero c

La funzione che cerchiamo deve smontare gli alberi dati in parallelo e ricavare i due argomenti della funzione data per ricreare il nuovo nodo.

zipAlberoWith f (Albero x xs) (Albero y ys) = Albero (f x y) $ zipWith (zipAlberoWith f) xs ys

Per zippare le liste di sottonodi utilizziamo lo stesso concetto per le liste. zipWith è una funzione della libreria di base.

*Main> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]

Proviamo zipAlberoWith

*Main> let x = Albero 0 [Albero 1 [Albero 2 [],Albero 3 []],Albero 4 []]
*Main> let y = Albero 3 [Albero 4 [Albero 1 [],Albero 2 []],Albero 0 []]
*Main> zipAlberoWith (*) x y
Albero 0 [Albero 4 [Albero 2 [],Albero 6 []],Albero 0 []]

I nodi sono stati moltiplicati membro a membro.

Facciamo qualcosa con le semirette

*Main> let x = Albero 20 [Albero 15 [],Albero 30 [Albero 10 []], Albero 40 [Albero 5 []],Albero 50 [Albero 12 []], Albero (-10) [Albero (-30.0) []]]
*Main> zipAlberoWith (\(Semiretta p t) r -> Semiretta p (t + r)) marionetta x
Albero (Semiretta (Punto (0.0,50.0)) 290.0) 
  [ Albero (Semiretta (Punto (0.0,60.0)) 105.0) []
  , Albero (Semiretta (Punto (15.0,45.0)) 330.0) [Albero (Semiretta (Punto (45.0,-5.0)) (-25.0)) []]
  , Albero (Semiretta (Punto (-15.0,45.0)) 340.0) [Albero (Semiretta (Punto (-45.0,-5.0)) (-30.0)) []]
  , Albero (Semiretta (Punto (10.0,-30.0)) 350.0) [Albero (Semiretta (Punto (40.0,-85.0)) 282.0) []]
  , Albero (Semiretta (Punto (-10.0,-30.0)) 290.0) [Albero (Semiretta (Punto (-40.0,-85.0)) 240.0) []]
  ]

Abbiamo aggiunto alle semirette della marionetta un angolo diverso a seconda del nodo. Notare che ‘x’ ha la stessa forma di ‘marionetta’, definito nell’articolo pre-precedente.

Bene, siamo quasi in fondo.

Marionetta, logica del movimento.

Ora che abbiamo una struttura che tiene traccia delle dipendenze tra i pezzi della marionetta, concentriamoci sulle operazioni minime che possiamo eseguire sui nodi della struttura . Queste operazioni devono mantenere la configurazione dei pezzi coerente.
La cosa più semplice è la traslazione di una semiretta, che si riduce alla traslazione del suo punto. Diciamo che se una semiretta trasla , mantenendo quindi invariato il suo angolo, le semirette direttamente dipendenti devono traslare dello stesso valore.
Per la rotazione di una semiretta, le cose sono più sottili. Se una semiretta ruota , varia solo il suo angolo. Le semirette direttamente dipendenti subiscono uno spostamento pari alla rotazione del loro punto della stessa quantità d’angolo , intorno al punto della semiretta che ha ruotato.
Codice.

type Spostamento = Punto -> Punto

spostamento :: Spostamento -> Semiretta -> Semiretta
spostamento f (Semiretta p t) = Semiretta (f p) t

rotazione :: Angolo -> Semiretta -> (Semiretta, Spostamento)
rotazione r (Semiretta p t) = (Semiretta p r, ruota p (r - t))

Intanto diamo un sinonimo alle variazioni di punti.
Sono di tipo Spostamento tutte le funzioni che dato un punto ne creano uno nuovo.

La funzione spostamento esegue la spostamento di una semiretta. Al punto della semiretta viene applicata lo spostamento, l’angolo rimane invariato.

La funzione rotazione, imposta il valore d’angolo alla semiretta e costruisce un nuovo Spostamento, pari alla rotazione, intorno al punto della semiretta, della variazione di angolo subita dalla semiretta stessa. Notiamo che alla funzione ruota vengono passati due argomenti. Manca infatti il punto da ruotare. Ma uno spostamento è proprio una funzione che prende un punto e ne crea uno nuovo, quindi “ruota p t” è di tipo Spostamento, come vuole la firma della funzione rotazione.

Adesso bisogna fare un salto di qualità e comporre i due effetti.
Si potrebbe pensare che i due effetti semplicemente si sommano e si ripercuotono sulle semirette successive, ma non è esattamente così.
Infatti lo Spostamento creato dalla rotazione è valido solo per le semirette direttamente dipendenti. Esso infatti varia a seconda della distanza della semiretta che lo subisce dal centro di rotazione e quindi è corretto solo per i pezzi direttamente vincolati al pezzo che ruota.
Ciò nonostante esso deve lasciare il segno perché abbiamo detto che se una semiretta trasla tutte le semirette dipendenti devono traslare ugualmente.
Quindi, quando spostiamo il punto di una semiretta a causa di una rotazione, questo spostamento diventa traslazione per le successive.
Creiamo quindi un valore che lasci separati i due effetti.


data Movimento = Movimento Spostamento Spostamento

spiazzamento :: Semiretta -> Semiretta -> Spostamento
spiazzamento (Semiretta q0 t) (Semiretta q1 r) p = p + (q1 - q0)

movimento :: (Angolo , Semiretta) -> Movimento -> (Semiretta, Movimento)
movimento (r,s) (Movimento rot tras) = let
   (rot', s') = rotazione r . spostamento rot . spostamento tras $ s
   in (s' , Movimento rot' (spiazzamento s s'))

Il datatype Movimento contiene i due spostamenti separati derivanti dalla rotazione del pezzo precedente e dalla sua rotazione.

La funzione spiazzamento ci serve a calcolare la traslazione finale del punto della semiretta, per passarlo alle successive.

La marionetta , astrazione I°

Il prossimo progetto è definire una Picture parametrica che rappresenti una marionetta con le posizioni regolabili dai parametri.
I parametri dovranno infine essere riportati alle altezze dei fili che sostengono la marionetta.
Svilupperemo un modello di marionetta in 2d.
Ecco il risultato dove facciamo ruotare il corpo e basta.
marionetta gira solo corpo
Ed ecco il risultato di una interpolazione tra varie configurazioni della marionetta.
marionetta interpolante

Astrazione

Una figura rigida in 2 dimensioni è completamente piazzata da un punto e una direzione con verso. Il punto fissa uno dei suoi punti che lo consideriamo centro di rotazione. La direzione con verso fornisce l’angolo di rotazione intorno a quel punto.

data Punto = Punto (Float,Float) deriving (Eq,Show)

Definiamo un nuovo dato che rappresenta i punti nel piano. Potevamo accontentarci di un sinomimo di (Float,Float). La dichiarazione finisce con una clausola “deriving” con argomento la classe Show ed Eq. Questo significa che chiediamo al compilatore di costruire per noi le istanze di quelle classi per il datatype Punto.
Le istanze della classe Show dichiarano una funzione show che trasforma i loro valori in String, per permetterci di stamparli.
Le istanze della classe Eq dichiarano una funzione (==) , quindi un operatore, che ci permette di confrontare due valori per l’uguaglianza. Proviamole.

*Main> let p = Punto (4,6)
*Main> show p
"Punto (4.0,6.0)"
*Main> p == p
True
*Main> p == Punto (4,5)
False

Ed ora vediamo i tipi di show e (==)

*Main> :t show
show :: Show a => a -> String
*Main> :t (==)
(==) :: Eq a => a -> a -> Bool

La firma di show si legge : “se il tipo ‘a’ è istanza di Show allora show va da ‘a’ a Sring”
La firma di (==) si legge : “se il tipo ‘a’ è istanza di Eq allora (==) va da ‘a’ a ‘a’ a Bool”

Per la nostra astrazione useremo molto la somma di punti, per definire gli spostamenti.
Il compilatore non può inventarsi le istanze di Num , classe che introduce l’operatore (+) nelle sue istanze, e quindi la scriviamo. La classe Num introduce molto di più della somma, ma a noi non serve altro e quindi definiamo queste funzioni come errori. La funzione error fa abortire il programma.
La funzione negate serve a costruire la differenza che ci tornerà utile ed ha una definizione ovvia, l’inverso della ascissa e dell’ordinata.

instance Num Punto where
	Punto (x,y) + Punto (x1,y1) = Punto (x+x1,y+y1)
	negate (Punto (x,y)) = Punto (negate x,negate y)
	(*) = error "Punto Num method undefined used"
	abs = error "Punto Num method undefined used"
	signum = error "Punto Num method undefined used"
	fromInteger = error "Punto Num method undefined used"

Proviamo somma e differenza

*Main> let p = Punto (4,6)
*Main> let q = Punto (3,2)
*Main> p + q
Punto (7.0,8.0)
*Main> p + q == q + p
True
*Main> p - q
Punto (1.0,4.0)
*Main> p + (negate q)
Punto (1.0,4.0)
*Main> 

Per gli angoli facciamo un sinonimo per essere più chiari nelle firme.

type Angolo = Float

Una funzione che sicuramente ci tornerà utile ci calcola il punto che si ottiene ruotando un punto intorno ad un altro dato un angolo. Gli argomenti sono in ordine: il centro di rotazione, l’angolo e il punto da ruotare.

ruota :: Punto -> Angolo -> Punto -> Punto
ruota q x p = let
	Punto (a,o)  = p - q  -- calcola il vettore da ruotare e lo smonta in ascissa e ordinata
	z = x*pi/180 + atan2 o a -- aggiunge l'angolo in argomento all'angolo del vettore
	r = sqrt (a ^ 2 + o ^ 2) -- calcola la lunghezza del vettore
        in q + Punto (r * cos z, r * sin z) -- calcola il vettore ruotato e lo sposta nel centro di rotazione

Al di là della semplice matematica, con l’utilizzo della funzione atan2 fornita dalla libreria di base, dobbiamo notare la presenza del costruttore Punto in due posti.
Nel secondo esso costruisce il nuovo punto.
Nel primo esso viene utilizzato per smontare il valore ottenuto da “p-q”, per conoscerne le coordinate: questa tecnica si chiama “pattern matching” e chiude il cerchio dei datatype, permettendoci appunto di smontarli. La forza del pattern matching ci risulterà più chiara quando affronteremo dei datatype più complessi.

Ora definiamo una semiretta.

data Semiretta = Semiretta Punto Angolo deriving (Show,Eq)

Una semiretta contiene un punto ed un angolo. Possiamo ora definire una marionetta come un insieme di semirette , ognuna rappresentante un pezzo.
Possiamo anche dire che due insiemi di semirette sono la stessa marionetta in configurazioni diverse se riscontriamo nei due insiemi alcune caratteristiche, che non sono semplici da dire.
Potremmo analizzare le relazioni tra i pezzi e imporre delle regole sulle distanze tra i punti delle semirette, ma invece seguiamo una strada costruttiva.
Intanto osserviamo che i perni che legano i pezzi formano un grafo aciclico. Per fortuna, perché i datatype ricorsivi si prestano a descrivere questo tipo di grafi.
Se osserviamo le marionette delle immagini iniziali e partiamo da uno qualsiasi dei loro pezzi, ci accorgiamo che possiamo sempre raggiungere tutti gli altri pezzi senza dover passare da ogni giunto più di una volta. Questa si chiama aciclicità.
Osserviamo che un grafo aciclico si può sempre trasformare in un albero, “tirando in su” uno qualsiasi dei nodi e lasciando appesi gli altri. Questa forma di dati, ad albero, si descrive facilmente in haskell.
Sappiamo che in ogni nodo ci metteremo una semiretta ma vogliamo essere più generali e pensare ad un albero che contenga valori di un tipo qualsiasi. L’albero che ci serve deve avere più sottonodi per ogni nodo. Per esempio al corpo della marionetta sono attaccate testa, braccia e gambe, 5 sottonodi. Questo tipo di albero si chiama rose-tree e si definisce così:

data Albero a = Albero a [Albero a] deriving (Show)

Si legge : “I valori di tipo ‘Albero a’ sono costruiti dal costruttore Albero e contengono un valore di tipo ‘a’ ed una lista di valori di tipo ‘Albero a’”.
Il valore di tipo ‘a’ è per esempio una semiretta , mentre la lista sono i sottonodi.
Ogni sottonodo è a sua volta un valore di tipo ‘Albero a’ e il gioco si ripete.
Definiamone uno di numeri.

let a1 = Albero 34 [Albero 12 [],Albero 31 []]

Il nodo in cima è il 34 ed ha due sottonodi 12 e 31, entrambi non hanno sottonodi.
Uno di nomi

let a1 = Albero "mimmo" [Albero "carlo" [Albero "francesco" [], Albero "giacomo" []],Albero "piero" []]

Questo ha 3 livelli ma è asimmetrico: il ramo piero finisce lì, mentre carlo ha due sottonodi.

.
Evidentemente per apprezzare un albero , bisogna dare un significato alla struttura. Nel caso della marionetta il significato è che nei sottonodi troviamo le semirette dei pezzi vincolati al pezzo descritto dalla semiretta nel nodo.


testa 		= Semiretta (Punto (0 ,60 )) 90 
corpo		= Semiretta (Punto (0 ,50 )) 270 
bracciodx 	= Semiretta (Punto (15 ,45 )) 300 
avambracciodx 	= Semiretta (Punto (45 ,-5)) (-35) 
cosciadx 	= Semiretta (Punto (10 , -30)) 300 
gambadx 	= Semiretta (Punto (40 , -85)) 270 

simmetrico (Semiretta (Punto (x,y)) t) = Semiretta (Punto (-x,y)) t

marionetta :: Albero Semiretta
marionetta = Albero corpo 
	[	Albero testa []
	,	Albero bracciodx [Albero avambracciodx []]
	,	Albero (simmetrico bracciodx) [Albero (simmetrico avambracciodx) []]
	,	Albero cosciadx [Albero gambadx []]
	,	Albero (simmetrico cosciadx) [Albero (simmetrico gambadx) []]
	]

Terzo esempio spiegato.

import Graphics.Gloss

main = displayInWindow "terzo esempio" (600,500) (0,0) white .
   Pictures . map f $ [100,99.9 .. 5]
   where f y    = Rotate (10*y) 
                . Translate 0 y 
                . Color (makeColor 0 (y/100) (1 - y/100) (y/100)) 
                $ ThickCircle y y

Per prima cosa notiamo una nuova parola chiave where. Con essa possiamo usare dei nomi nelle nostre definizioni che specifichiamo dopo il where.
Nel terzo esempio abbiamo usato il nome ‘f’ definendolo in seguito.

f x = g (g x) where g x = x * x

Questo stralcio si legge “f di x è uguale a g di g di x dove g x è uguale a x per x”.
La definizione di ‘f’ non ci da problemi e segue lo stile composizionale spiegato nell’articolo precedente. Infatti abbiamo aggiunto una funzione alla catena delle composizioni, un costruttore che aggiunge il colore.
La funzione ‘f’ crea per ogni ‘y’ un cerchio pieno di raggio ‘y’ di colore verde per ‘y/100′ , blu per ’1-y/100′ e trasparenza ‘y/100′, spostato lungo la verticale di ‘y’ pixel e ruotato di ’10*y’ gradi intorno all’origine.
Ora introduciamo map che è una funzione di ordine superiore (HOF) , la seconda che incontriamo. Le HOF sono funzioni che prendono funzioni come argomento. La prima era il (.).
Le HOF sono strumenti potenti che è necessario imparare per dominare i linguaggi funzionali e comunque sono molto utili per riutilizzare il codice anche negli altri.

Prelude> :t map
map :: (a -> b) -> [a] -> [b]

Il primo argomento di map una funzione da ‘a’ a ‘b’ che quindi possono essere qualsiasi tipo.
Il secondo e ultimo argomento è una lista di ‘a’. Il risultato una lista di ‘b’.
Abbiamo già capito che map applica la funzione passata a tutti gli elementi della lista, ottenendo una nuova lista.

Prelude> map not [True,False,False,True]
[False,True,True,False]

La prima considerazione è che map si ottiene facilmente con la list comprehension che invece permette anche altro.

Prelude> [not x | x <- [True,False,False,True]]
[False,True,True,False]

La seconda è che map è molto limitato, infatti non riusciamo a calcolare un valore che considera insieme tutti gli elementi della lista.
Per esempio non siamo in grado di sommare gli elementi di una lista.
Ora completiamo la nostra conoscenza della list comprehension, introducendo il concetto di guardia, ma per arrivarci facciamo il percorso contrario a quello fatto per map.
Una nuova HOF: filter.

Prelude> :t filter
filter :: (a -> Bool) -> [a] -> [a]

filter riceve una funzione giudice e una lista . Con la funzione seleziona gli elementi giudicati True e li mette nella nuova lista creata.

Prelude> filter odd [1..20]
[1,3,5,7,9,11,13,15,17,19]

Per sicurezza controlliamo che tipo di odd sia compatibile con il primo argomento di filter, che restituisce True se il suo argomento è dispari

Prelude> :t odd
odd :: Integral a => a -> Bool

Ora scriviamo una funzione che ci restituisce il carattere relativo ai numeri dispari tra 30 e 70

Prelude Data.Char> map chr . filter odd $ [30 .. 70]
"\US!#%')+-/13579;=?ACE"

La list comprehension permette di introdurre una guardia che agisce esattamente come filtro.

Prelude Data.Char> [chr x | x <- [30 .. 70], odd x]
"\US!#%')+-/13579;=?ACE"

La potenza della list comprehension viene dalla possibilità di nidificare più generatori

Prelude Data.Char> [x + y | x <- [1 .. 9], y <- [1 .. 9],  odd x && even y]
[3,5,7,9,5,7,9,11,7,9,11,13,9,11,13,15,11,13,15,17]

Per finire introduciamo un costrutto simile a where che per ora utilizzeremo senza approfondirne le differenze con where.
where posticipa le definizioni mentre let … in le anticipa. Una differenza per noi sostanziale è che lo possiamo utilizzare in ghci per i nostri esempi.

Prelude Data.Char> let f x = x `mod` 3 == 0 in filter f [1..30]
[3,6,9,12,15,18,21,24,27,30]

Qui definiamo una funzione ‘f’ che ritorna True quando il suo argomento è multiplo di 3 e poi la utilizziamo come argomento di filter.
Ed ecco il nostro terzo esempio riscritto con let … in

import Graphics.Gloss

main = let 
	f y    = Rotate (10*y) . Translate 0 y
               . Color (makeColor 0 (y/100) (1 - y/100) (y/100))
               $ ThickCircle y y
	p = Pictures $ map f [100,99.9 .. 5]	
	in displayInWindow "terzo esempio" (600,500) (0,0) white p

Secondo esempio, i combinatori.

Per finire i commenti sul secondo esempio esaminiamo le differenze tra l’utilizzo della composizione di funzioni e dell’applicazione a bassa precedenza , rispetto all’uso di parentesi.
Per esempio, ammettiamo di voler contare gli spicchi di un’arancia. Per farlo dobbiamo pelarla e dividerla. Quindi per descrivere l’operazione a chi deve eseguirla scriveremmo su un foglio “pela, dividi e conta”. Se volessimo scrivere il programma useremmo le parentesi per dare la precedenza alle operazioni.

contaspicchi x = conta (dividi (pela x))

Notiamo tuttavia che pela, dividi e conta hanno la stessa struttura, ricevono una cosa in ingresso e ne fanno uscire una nuova. Proviamo a scrivere le firme.

pela :: Arancia -> Pelata
dividi :: Pelata -> Spicchi
conta :: Spicchi -> Int

La loro combinazione è contaspicchi

contaspicchi :: Arancia -> Int

Allora ci inventiamo una funzione per combinare questo tipo di operazioni. Chiamiamola e.
La definizione di contaspicchi sarebbe:

contaspicchi = e conta  (e dividi pela)

Ecco la definizione di e

e f g x = f (g x)

Così non sarebbe un gran che, ma haskell ci permette di usare dei simboli al posto delle funzioni e la funzione e è già nella libreria di base sotto forma di operatore .

Ma prima di tutto vediamo la sottile differenza tra operatore e funzione.
Gli operatori sono funzioni di esattamente due argomenti.
Il loro nome contiene solo simboli che non possono essere utilizzati nei nomi di funzioni generiche.
La loro posizione è sempre in mezzo ai loro due argomenti.
Vediamone un paio famosi.

Prelude> :t (+)
(+) :: Num a => a -> a -> a
Prelude> :t (-)
(-) :: Num a => a -> a -> a

Le parentesi intorno ad un operatore lo trasformano in funzione.

Prelude> 3 + 2
5
Prelude> (+) 3 2
5

Quindi quando osserviamo la firma di un operatore la troveremo con l’operatore usato come funzione, ovvero con le parentesi intorno.
L’operatore . che si chiama combinazione di funzione è funzione di due argomenti.

Prelude> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

Sgrunt, ma qui gli argomenti sono 3.. allora lo riscriviamo così:

(.) :: (b -> c) -> (a -> b) -> (a -> c)

In seguito si capirà che questa operazione è corretta.
Il primo argomento è una funzione che dal tipo ‘b’ va al tipo ‘c’, scegliamo isUpper che valuta se un carattere è maiuscolo o meno.

Prelude Data.Char> :t isUpper 
isUpper :: Char -> Bool

Quindi in questo caso ‘b’ è Char e ‘c’ è Bool.
Dalla firma di (.) scegliamo una funzione da ‘a’ a ‘b’, con ‘b’ fissato a Char dalla prima. La funzione è chr

Prelude Data.Char> :t chr
chr :: Int -> Char

Questa fissa ‘a’ ad essere Int.

Riscrivendo la firma del combinatore con i tipi scelti abbiamo:

(.) :: (Char -> Bool) -> (Int -> Char) -> (Int -> Bool)

Vediamo cosa dice l’interprete.

Prelude Data.Char> :t isUpper . chr
isUpper . chr :: Int -> Bool

Possiamo usare questa funzione per vedere i numeri delle lettere maiuscole

Prelude Data.Char> [(x,chr x, (isUpper . chr) x) | x <- [60 .. 80]]
[(60,'',False),(63,'?',False),(64,'@',False),(65,'A',True),(66,'B',True),(67,'C',True),(68,'D',True),(69,'E',True),(70,'F',True),(71,'G',True),(72,'H',True),(73,'I',True),(74,'J',True),(75,'K',True),(76,'L',True),(77,'M',True),(78,'N',True),(79,'O',True),(80,'P',True)]

L’ultimo sforzo serve ad eliminare le parentesi in (isUpper. chr) x che ora sappiamo coincidere con isUpper (chr x).
Per farlo introduciamo l’applicazione di funzione esplicita di ($).
Quando scriviamo f x intendiamo la funzione f applicata ad x, quindi se f :: a -> b allora x :: a e f x :: b.
L’operatore ($) sostituisce l’applicazione e quindi f $ x = f x ma, mentre l’applicazione scritta con lo spazio ha precedenza altissima, quella scritta con ($) ha precedenza bassissima e comunque più bassa di (.) .
Risultato (isUpper . chr) x si può scrivere isUpper . chr $ x ed è così che lo troveremo scritto spesso nel codice haskell.

Tornando infine al nostro esempio

Rotate (2*y) . Translate 0 y $ Circle y

riconosciamo il pattern f . g $ x che quindi sappiamo riscrivere f (g x) ovvero la forma naturale con parentesi Rotate (2*y) (Translate 0 y (Circle y))

Secondo esempio spiegato.

Per ragionare meglio sul secondo esempio, lo riscriviamo scomponendo l’espressione che determina il valore di tipo Picture.

import Graphics.Gloss
f y = Rotate (2*y) . Translate 0 y $ Circle y
ps = [f y | y <- [5,10 .. 100]]
main = displayInWindow "secondo esempio" (500,600) (0,0) white $ Pictures ps

Per caricare questa definizione in ghci, si deve salvare il codice in un file “esempio2.hs”, aprire ghci e usare il comando :load

Prelude> :load esempio2
[1 of 1] Compiling Main             ( esempio2.hs, interpreted )
Ok, modules loaded: Main.
*Main> 

Ora possiamo interrogare l’interprete per conoscere i tipi dei nomi caricati

*Main> :t f
f :: Float -> Picture
*Main> :t ps
ps :: [Picture]

Il tipo di f è lo stesso di Circle. Infatti il significato dell’espressione è: un cerchio di raggio y , spostato di y pixel in alto e ruotato intorno all’origine di 2*y gradi. Quindi una funzione che da qualsiasi y di tipo Float crea una Picture.
Andiamo piano. Prima di tutto eliminiamo il . e l’$ che sono combinatori e ci complicano la vita all’inizio. Utilizziamo le parentesi per isolare le sottoespressioni.

f y = Rotate (2*y) (Translate 0 y (Circle y))

L’espressione è equivalente ma meno idiomatica. Osseviamo i tipi dei 3 costruttori in gioco.

*Main> :t Circle
Circle :: Float -> Picture
*Main> :t Translate
Translate :: Float -> Float -> Picture -> Picture
*Main> :t Rotate
Rotate :: Float -> Picture -> Picture
*Main> 

C’è qualcosa che distingue nettamente Circle dagli altri. Infatti Circle è l’unico che pare costruisca una Picture, gli altri la trasformano. Ma in haskell le cose non si trasformano, le cose si creano e basta. Allora si capiscono meglio i tipi di Translate e Rotate. Translate vuole i due spiazzamenti orizzontale e verticale e una Picture, così facendo esso è un’altra Picture. Stesso vale per Rotate che vuole un angolo ed una Picture, risultando in una nuova Picture.

Questa corrispondenza tra la descrizione e il codice è la forza dei datatype ricorsivi: essi sono un buon modello per un linguaggio descrittivo.
I datatype ricorsivi contengono valori del tipo che stanno defininendo nella definizione dei propri costruttori.
Dobbiamo capire che essi sono semplicemente un valore, come 42 o “primo esempio” o 12.34, ma descrivono un valore che ha una struttura complessa.
Scriviamo alcuni valori di tipo Picture.

*Main> :t Translate 10 10 (Circle 10)
Translate 10 10 (Circle 10) :: Picture
*Main> :t Rotate 40 (Circle 20)
Rotate 40 (Circle 20) :: Picture
*Main> :t Translate 10 10 (Rotate 40 (Circle 20))
Translate 10 10 (Rotate 40 (Circle 20)) :: Picture
*Main> 

Il secondo ostacolo è il valore ps.

ps :: [Picture]

Il suo tipo si legge “Lista di valori di tipo Picture”. Purtroppo la lista è il primo tipo parametrico che incontriamo, ed ha una sintassi un po speciale che confonde molto le idee. Una lista è un tipo ricorsivo esattamente come Picture, ma è polimorfo, ovvero è di tipo diverso a seconda del tipo degli elementi che contiene.
Questa cosa riflette il suo significato, una lista di numeri è diversa da una lista di lettere, diversa da una lista di Picture e così via. Siccome la lista è omni presente nel codice haskell, una sintassi speciale è stata introdotta, in particolare il suo tipo si scrive [] e il suo parametro, il tipo degli elementi che contiene si scrive all’interno delle quadre. Anche il costruttore di valori lista si scrive [] e l’elenco dei valori contenuti si mette dentro separato da virgole.

*Main> :t ['a','b','c','d']
['a','b','c','d'] :: [Char]

Questa era una lista di caratteri. Il suo tipo [Char] si può scrivere [] Char. Il valore è stato costruito con parentesi quadre e elementi separati da virgole.
Nell’esempio ho usato una sintassi speciale per le liste (e non solo) che si chiama list comprehension e un operatore anch’esso speciale che genera sequenze dati gli estremi.
Vediamo prima l’operatore .

Prelude> :t ['a' .. 'z']
['a' .. 'z'] :: [Char]
Prelude> [1 .. 10]
[1,2,3,4,5,6,7,8,9,10]
Prelude> [1,3 .. 10]
[1,3,5,7,9]
Prelude> [10,8 .. -10]
[10,8,6,4,2,0,-2,-4,-6,-8,-10]
Prelude> 

E ora la list comprehension.

Prelude> [x * 2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]

L’operazione costruisce una lista moltiplicando per 2 tutti gli elementi di [1 .. 10]. Prima della barra verticale scriviamo l’espressione che descrive un elemento della lista, dopo la barra descriviamo l’assegnazione degli elementi ai nomi che appaiono nell’espressione.
Nell’esempio sopra x * 2 impone che un elemento è il doppio del valore del nome x. Mentre x <- [1 .. 10] impone che il nome x assuma i valori da 1 a 10 nell’espressione dell’elemento.


Prelude> [(x,y) | x <- ['a' .. 'c'], y <- [1 .. 3]]
[('a',1),('a',2),('a',3),('b',1),('b',2),('b',3),('c',1),('c',2),('c',3)]

Qui creiamo delle coppie dette tuple. Da notare che gli assegnamenti alla ‘x’ e alla ‘y’ non avvengono in parallelo , ma ogni elemento della ‘x’ viene accoppiato con ogni elemento ‘y’.

Tornando a ps, ps è una lista di elementi creati applicando la funzione ‘f’ ad ogni ‘y’ dove ‘y’ varia tra 5 e 100 con passo 5. Ma la funzione ‘f’ genera una Picture da un Float, quindi i valori assegnati a ‘y’ sono Float e ps è una lista di Picture.

Possiamo vedere i primi valori di ps che sono i cerchi che compongono la nostra figura.

*Main> take 3 ps
[Rotate 10.0 (Translate 0.0 5.0 (Circle 5.0)),Rotate 20.0 (Translate 0.0 10.0 (Circle 10.0)),Rotate 30.0 (Translate 0.0 15.0 (Circle 15.0))]

Per finire esaminiamo il costruttore Pictures, infatti l’ultimo argomento di displayInWindow deve essere di tipo Picture, mentre ps è di tipo [Picture].

*Main> :t Pictures
Pictures :: [Picture] -> Picture
*Main> :t ps
ps :: [Picture]
*Main> :t Pictures ps
Pictures ps :: Picture

Esattamente il necessario per accontentare displayInWindow.

Primi passi spiegati

Nell’articolo precedente abbiamo introdotto la libreria gloss per accompagnare il percorso sulla programmazione in haskell con qualcosa di concreto.
Ora, esaminando i tre stralci di codice , cerchiamo di capire le basi.
Tutti i programmi di un certo livello si appoggiano ad altri di livello inferiore. Tutti i file contenenti codice si dicono moduli. Nelle prime righe di ogni modulo sono inserite le dipendenze dagli altri.
La parola speciale è import seguita dal nome del modulo che contiene le definizioni che intendiamo utilizzare.
Nei nostri esempi abbiamo importato il modulo Graphics.Gloss. Questo modulo esporta varie definizioni che ci servono a disegnare.
Negli esempi abbiamo una sola definizione a livello di file, quella del nome main.
Ad esso viene associata la funzione displayInWindow con i suoi parametri.
In haskell tutti i nomi che iniziano con lettera minuscola hanno un tipo associato, che determina la correttezza nell’utilizzo.
Dalla documentazione sulla funzione displayInWindow, essa riceve 5 argomenti: il nome della finestra, la grandezza della finestra, le coordinate dell’angolo alto sinistro, il colore di fondo e infine un valore di tipo Picture che definisce il disegno.
Nel primo esempio il valore di tipo Picture è Circle 100. Circle è una funzione costruttore di valori.
Per costruire un valore di tipo Picture possiamo usare uno qualsiasi dei costruttori del suo datatype, quindi Blank, Circle, Polygon e gli altri, ognuno corredato dai suoi argomenti di tipo corretto.
Per esaminare meglio usiamo ghci, l’interprete di ghc.
L’interprete non è potente come il compilatore, ma è molto flessibile e istruttivo.
Si lancia da riga di comando.

paolino@paolino-desktop:~$ ghci
GHCi, version 7.2.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Prelude> import Graphics.Gloss
Prelude Graphics.Gloss> :type Circle
Circle :: Float -> Picture
Prelude Graphics.Gloss> :info Circle
data Picture = ... | Circle Float | ...
  	-- Defined in Graphics.Gloss.Data.Picture

Qui sopra ho riportato una sessione ghci dove ho importato Gloss, ho chiesto il tipo e le informazioni riguardanti Circle.
Come possiamo notare il tipo di Circle è Float -> Picture, che si legge “dato un valore di tipo Float, produce un valore di tipo Picture”. Dalla documentazione apprendiamo che il valore di tipo Float corrisponde al raggio del cerchio. Il simbolo :: si legge “è di tipo”.
Mentre al simbolo = corrisponde una definizione, il simbolo :: corrisponde ad una firma.

Prelude Graphics.Gloss> :t displayInWindow 
displayInWindow
  :: String -> (Int, Int) -> (Int, Int) -> Color -> Picture -> IO ()

Continuando la nostra ispezione notiamo i tipi degli argomenti di displayInWindow che corrispondono al significato dato prima. Controlliamo la coerenza del primo esempio.

Prelude Graphics.Gloss> :t "primo esempio"
"primo esempio" :: [Char]
Prelude Graphics.Gloss> :i String
type String = [Char] 	-- Defined in GHC.Base
Prelude Graphics.Gloss> :t (100,100)
(100,100) :: (Num t, Num t1) => (t, t1)
Prelude Graphics.Gloss> :t (0,0)
(0,0) :: (Num t, Num t1) => (t, t1)
Prelude Graphics.Gloss> :t white
white :: Color
Prelude Graphics.Gloss> :t Circle 100
Circle 100 :: Picture
Prelude Graphics.Gloss> 

Non è andata benissimo, ma su qualcosa ci siamo da subito, white è di tipo Color , e Circle 100 è di tipo Picture. Gli ultimi due argomenti di displayInWindows sono corretti.

Per il primo ho dovuto chiedere informazioni rispetto a String, e , per fortuna il mistero è risolto String è definito come [Char]. Qui si introduce il concetto di sinonimo di tipo. La parola magica è type.

type String = [Char]

Quindi String e [Char] sono lo stesso tipo.
Se continuiamo l’indagine le cose si complicano e si introduce la omni presente struttura dei programmi funzionali, la lista. Infatti [Char] si legge “lista di caratteri”, ma rimandiamo la cosa, per ora, visto che non ci mettiamo a smontare il valore “primo esempio”.

Per gli argomenti dimensione e piazzamento della finestra, i risultati dell’interrogazione sono imprevisti.

Prelude Graphics.Gloss> :t (300,300)
(300,300) :: (Num t, Num t1) => (t, t1)

Ci aspettavamo

(300,300) :: (Int, Int)

, invece l’interprete ci ha restituito

(t,t1)

lasciandoci capire che il tipo di 300 non è fissato. In compenso ha aggiunto una parte nuova nella risposta

(Num t, Num t1) => 

che si legge “se t è istanza della classe Num e t1 è istanza della classe Num, allora …”.
Senza entrare nelle classi di tipi, cerchiamo di capire il problema.
L’argomento di Circle nel primo esempio è 100, ma Circle è di tipo Float -> Picture, quindi 100 è di tipo Float quando lo mettiamo dopo Circle, invece se lo mettiamo come argomento di displayInWindow diventa di tipo Int. Qualcosa non va.

Prelude Graphics.Gloss> :t 100
100 :: Num a => a

Si può leggere 100 è di un tipo qualsiasi a patto che il tipo sia un numero. Ora suona meglio.
La domanda sorge spontanea, quanti tipi che sono numeri esistono ? Qualsiasi tipo che implementi un certo insieme di funzionalità e di comportamenti è un numero.
Non a caso abbiamo i numeri interi, i razionali e i reali anche nella nostra matematica.
La classe Num contiene le funzionalità da implementare per i tipi che ne vogliono essere istanze.
Tanto per curiosare, interroghiamo l’interprete.

Prelude Graphics.Gloss> :i Num
class (Eq a, Show a) => Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  	-- Defined in GHC.Num
instance Num Point -- Defined in Graphics.Gloss.Data.Point
instance Num Integer -- Defined in GHC.Num
instance Num Int -- Defined in GHC.Num
instance Num Float -- Defined in GHC.Float
instance Num Double -- Defined in GHC.Float

Alcune cose erano prevedibili, un numero deve potersi sommare ad un altro, moltiplicare e sottrarre , negare …
Altre sono più tecniche e ci interessano meno.
La lista delle istanze che segue la classe, invece, contiene un particolare interessante.

instance Num Point -- Defined in Graphics.Gloss.Data.Point

La libreria gloss, nel suo modulo Graphics.Gloss.Data.Point ha creato un istanza di Num nuova: l’istanza di Point.
Ora siamo più curiosi.

Prelude Graphics.Gloss> :i Point
type Point = (Float, Float)
  	-- Defined in Graphics.Gloss.Data.Point

Un altro sinonimo di tipo, una coppia di tipi Float. Non ci resta che testarne l’appartenenza alla classe Num

Prelude Graphics.Gloss> (4,5) + (7,1) :: Point
(11.0,6.0)

Ottimo, a parte che ho dovuto annotare l’espressione con :: Point , ma questi sono prezzi da pagare quando si gioca con certe istanze di cui un giorno forse parleremo.