Haskell: che tipo vuoi?

By abell on 2009-03-24-21:14:38 | In haskell polimorfismo quickcheck tipi

Diverse cose sono fonte di meraviglia per chi si avvicina ad Haskell. Molti dei linguaggi di uso comune (Java, C, C++, perl, Python e simili) hanno una qualche forma di polimorfismo, che permette di applicare uno stesso metodo o funzione a tipi diversi, con un risultato che dipende dal tipo degli argomenti o dell'oggetto su cui viene invocato.

Ad esempio:
1 + 3 => 4
"questo" + "quello" => "questoquello"
"a".clone() => una nuova stringa "a"
connection.clone() => un nuovo oggetto di classe Connection
In perl esiste anche un tipo (limitato) di dipendenza inversa, in cui il tipo di risultato dipende non dagli argomenti, ma dal contesto in cui il metodo viene chiamato.
@a = localtime(); # list context
# @a contiene ora una lista con secondi, minuti, ore etc., tipo ( 45, 42, 20, 24, 2, 109, 2, 82, 0 )
$a = localtime(); # scalar context
# $a contiene ora la stringa "Tue Mar 24 20:44:07 2009"

In Haskell l'inferenza dei tipi fa sì che il tipo della funzione invocata dipende dalle informazioni che il type checker ricava dal contesto in cui viene chiamata.

In altri linguaggi, di solito la determinazione del tipo avviene in forma "push", ovvero viene spinta dagli argomenti al risultato. In Haskell, sorprendentemente è possibile definire funzioni come la seguente:

quellochevuoi :: a

La funzione quellochevuoi, se invocata, restituisce un valore che può essere di tipo qualsiasi, di un generico a che può a seconda dei casi essere un Int, una stringa, una funzione, un array di funzioni o altro. Il compilatore "sa" che il risultato di quellochevuoi, per il contesto in cui viene usato, deve essere di un certo tipo e sceglie la giusta realizzazione della funzione di conseguenza. Nei casi in cui non lo "sappia", cioè quando il contesto dell'invocazione non è sufficiente a determinare il tipo di a, si possono fornire annotazioni di tipo nel sorgente.

Facciamo un esempio concreto.

Definiamo una classe
class UnoQualsiasi a
    where
      unoQualsiasi :: a

Per ogni tipo della classe UnoQualsiasi sarà possibile avere un oggetto rappresentativo, tramite la funzione unoQualsiasi (ricordiamo che i tipi e le classi in Haskell hanno iniziale maiuscola, mentre la minuscola è per funzioni e costanti).

Definiamo alcune istanze:

instance UnoQualsiasi Int
    where
      unoQualsiasi = 42
abbiamo scelto 42 come l'esempio di intero qualsiasi
instance UnoQualsiasi Char
    where
      unoQualsiasi = 'x'
'x' è il carattere qualsiasi
instance UnoQualsiasi a => UnoQualsiasi [ a ]
    where
      unoQualsiasi = replicate 10 unoQualsiasi
10 cose qualsiasi di un certo tipo rappresentano un array qualsiasi (di oggetti di quel tipo) L'invocazione "se vuoi una stringa, ti do " ++ unoQualsiasi restituisce
"se vuoi una stringa, ti do xxxxxxxxxx"
(10 "caratteri qualsiasi") mentre se dico unoQualsiasi + unoQualsiasi il compilatore si lamenta, perché ho chiesto un numero e non sa se darmi un float o un intero o altro. Se specifico che voglio un Int:
unoQualsiasi + unoQualsiasi :: Int
ottengo 84 (42+42).

La libreria QuickCheck, usata per il testing, verifica il realizzarsi di certe proprietà generando degli argomenti a caso. E' sufficiente che gli argomenti siano di un tipo appartenente alla classe Arbitrary così definita:

class Arbitrary a where
  arbitrary   :: Gen a
...

A questo punto, arbitrary restituirà un Gen Int o Gen String o Gen tipo_complesso a seconda che la funzione da testare abbia bisogno di argomenti di un tipo o dell'altro. Nessuna magia, ma un po' ne ha il sapore.