Hai già familiarità con la programmazione "a livello di valore", per cui si manipolano valori come 42 :: Int
o 'a' :: Char
. In linguaggi come Haskell o Scala, la programmazione tipo di livello consente di manipolare tipi come Int :: *
o Char :: *
dove *
è il tipo di tipo concreto (Maybe a
o [a]
sono tipi concreti, ma non Maybe
o []
che hanno tipo * -> *
).
Ora confrontare foo
in questa famiglia di tipi, abilitata con l'estensione di lingua TypeFamilies
.
type family Foo (x :: *)
type instance Foo Char = Int
Qui Foo
prende un tipo di tipo*
e restituisce un nuovo tipo di genere *
utilizzando la semplice mappatura Char -> Int
. Questa è una funzione di livello di tipo che manipola i tipi.
Questo è un esempio molto semplice e ci si potrebbe chiedere come questo possa essere utile. Usando strumenti di linguaggio più potenti, possiamo iniziare a codificare le prove della correttezza del nostro codice a livello di testo (per ulteriori informazioni, consultare la corrispondenza Curry-Howard).
Un esempio pratico è un albero rosso-nero che utilizza la programmazione a livello di codice per garantire staticamente la tenuta degli invarianti dell'albero.
Un albero rosso-nero ha le seguenti proprietà semplici:
- Un nodo è rosso o nero.
- La radice è nera.
- Tutte le foglie sono nere. (Tutte le foglie hanno lo stesso colore della radice.)
- Ogni nodo rosso deve avere due nodi figlio neri. Ogni percorso da un determinato nodo a una delle sue foglie discendente contiene lo stesso numero di nodi neri .
Useremo DataKinds
e GADTs
, una potente combinazione di programmazione di livello tipo.
{-# LANGUAGE DataKinds, GADTS, KindSignatures #-}
import GHC.TypeLits
In primo luogo, alcuni tipi per rappresentare i colori.
data Colour = Red | Black -- promoted to types via DataKinds
questo definisce un nuovo tipo Colour
abitato da due tipi: Red
e Black
. Nota che non ci sono valori (ignorando i fondi) che abitano questi tipi, ma non avremo comunque bisogno di loro.
I nodi dell'albero rosso-neri sono rappresentati dai seguenti GADT
-- 'c' is the Colour of the node, either Red or Black
-- 'n' is the number of black child nodes, a type level Natural number
-- 'a' is the type of the values this node contains
data Node (c :: Colour) (n :: Nat) a where
-- all leaves are black
Leaf :: Node Black 1 a
-- black nodes can have children of either colour
B :: Node l n a -> a -> Node r n a -> Node Black (n + 1) a
-- red nodes can only have black children
R :: Node Black n a -> a -> Node Black n a -> Node Red n a
GADT
ci permette di esprimere la Colour
dei R
e B
costruttori direttamente nei tipi.
La radice dell'albero assomiglia a questo
data RedBlackTree a where
RBTree :: Node Black n a -> RedBlackTree a
Ora è impossibile creare un ben digitato RedBlackTree
che viola uno qualsiasi dei 4 immobili di cui sopra.
- Il primo vincolo è ovviamente vero, ci sono solo 2 tipi abitano
Colour
.
- Dalla definizione di
RedBlackTree
la radice è nera.
- Dalla definizione del costruttore
Leaf
, tutte le foglie sono nere.
- Dalla definizione del costruttore
R
, entrambi i figli devono essere i numeri Black
. Come pure, il numero di nodi figlio neri di ogni sottostruttura sono uguali (lo stesso n
viene utilizzato nel tipo di entrambe le sottostrutture sinistra e destra)
Tutte queste condizioni sono verificate al momento della compilazione da GHC, il che significa che abbiamo non otterrà mai un'eccezione di runtime da un codice anomalo che invalida le nostre ipotesi su un albero rosso-nero. È importante sottolineare che non ci sono costi di runtime associati a questi vantaggi extra, tutto il lavoro viene svolto in fase di compilazione.
Consiglio di leggere "Follia istantanea di tipo livello" in [Numero 8 di The Monad Reader] (http://www.haskell.org/wikiupload/d/dd/TMR-Issue8.pdf). –
Vedi anche: http://stackoverflow.com/questions/4415511/scala-type-programming-resources – Jesper