2010 de Marzo

Phantom Types con uuagc

March 24, 2010 por Carlos Gomez   Comentarios (3)

, , ,

Despues de entender Phantom Types, me puse a ver la manera de hacerlo con AGs (Attribute Grammar), y la verdad no es cosa de otro mundo. Pero veamoslo paso a paso.

Un Phantom Type, es una tecnica que nos permite hacer chequeo de tipos en tiempo de compilacion manteniendo una gramatica abstracta sencilla y comprensible.

Veamoslo con el ejemplo de la pagina fuente, tenemos un evaluador de expresiones, el cual es reescrito usando gramatica de attributos de esta manera:

DATA Expr
    | Val Int
    | Add le,lr : Expr
    | LE e1, e2 : Expr
    | Cond ex, iex, eex : Expr

DERIVING * : Show

ATTR Expr [ | | value : {Res}]

SEM Expr
    | Val lhs.value  = ResI @int
    | Add lhs.value  = case (@le.value, @lr.value) of
                               (ResI x, ResI y) -> ResI (x + y)
                               _                    -> error "Bad Arguments to Add"
    | LE lhs.value   = case (@e1.value, @e2.value) of
                               (ResI x, ResI y)  -> ResB (x <= y)
                               _                     -> error "Bad Arguments to LE"
    | Cond lhs.value = case (@ex.value) of
                               ResB b -> if b then @iex.value else @eex.value
                               _        -> error "Bad Arguments to Cond"

{
data Res = ResI Int | ResB Bool  deriving Show

main :: IO()
main = print e1
    where e1 = sem_Expr (Cond (LE (Val 3) (Val 5)) (Add (Val 1) (Val 2)) (Val 0)) t;br />             e2 = sem_Expr (Cond (Val 1) (Val 1) (Val 1))
}

Para probarlo, hacemos "uuagc -mdfcs AExpr.ag", y luego lo cargamos con "ghci AExpr.hs" y lo ejecutamos con "main", y el resultado sera "ResI 3".

Hasta aqui, todo anda bien, pero que pasa si en el ag cambiamos "e1", por "e2" en el "main"?despues ejecutarlo nos da un error de "Bad arguments to Cond", asi como lo esperabamos. Pero que pasaria si no queremos obtener estos errores, y que si quisieramos capturar estos errores en tiempo de compilacion?, una forma comun de hacerlo seria cambiando nuestro tipo algebraico y escribiendo una gramatica abstracta que tenga expresiones booleanas, enteras y demas cosas. Pero una forma es usando la tecnica de Phantom Type, como lo mensionamos mas antes, mantenemos una gramatica sencilla y nos permite usar el chequeo de tipos de Haskell.

Entonces, lo reescribimos de esta manera:

DATA Expr'
    | Val Int
    | Add le,lr : Expr'
    | LE e1, e2 : Expr'
    | Cond ex, iex, eex : Expr'

DERIVING * : Show

ATTR Expr' [ | | value : {Res}]
SEM Expr'
    | Val lhs.value  = ResI @int
    | Add lhs.value  = case (@le.value, @lr.value) of
                            (ResI x, ResI y) -> ResI (x + y)
                            _                    -> error "Bad Arguments to Add"
    | LE lhs.value   = case (@e1.value, @e2.value) of
                            (ResI x, ResI y)  -> ResB (x <= y)
                            _                     -> error "Bad Arguments to LE"
    | Cond lhs.value = case (@ex.value) of
                            ResB b -> if b then @iex.value else @eex.value
                            _        -> error "Bad Arguments to Cond"

{
data Res = ResI Int | ResB Bool  deriving Show

newtype Expr a = E Expr'
    deriving (Show)

-- wrappers
val :: Int -> Expr Int
val = E . Val

add :: Expr Int -> Expr Int -> Expr Int
add (E x) (E y) = E $ Add x y

le :: Expr Int -> Expr Int -> Expr Bool
le (E x) (E y) = E $ LE x y

cond :: Expr Bool -> Expr a -> Expr a -> Expr a
cond (E c) (E x) (E y) = E $ Cond c x y

eval :: Expr a -> Res
eval (E e) = sem_Expr' e

main :: IO()
main = print e1
    where e1 = eval (cond (le (val 3) (val 5)) (add (val 1) (val 2)) (val 0))
             --e2 = eval (cond (val 1) (val 1) (val 1))
}

Y ahora, ejecutamos lo mismo que la ultima ves y obtenemos el mismo resultado. Pero si cambiamos "e1" por "e2" en el "main", descomentamos "e2" e intentamos compilarlo, obtendremos un error de tipos en tiempo de compilacion, asi podremos contruir solo expresiones validas para nuestro evaluador.

Donde esta la tecnica de Phantom Type?, los hacemos atraves de "newtype Expr a = E Expr' ", el cual crea un nuevo tipo parametrizando el tipo deseado, el cual es controlado por el type chequer de Haskell. Este nuevo tipo, tiene en su contructor de datos algo asi como un tipo fantasma (Phantom Type), esto pienso asi porque:

-- el tipo del contructor de datos de Expr seria
E :: Expr' -> Expr a

-- por ejemplo el tipo de val, es
val :: Int -> Expr Int

Asi el tipo de "Expr a" no refleja el contenido que tiene empaquetado, por eso pienso que el tipo de adentro es el fantasma.

Volviendo al tema, el siguiente paso despues de crear el tipo fantasma, es escribir las funciones wrapper, los cuales se encargan de contruir el "Expr' " que queremos. Y tambien es aqui donde detallamos el chequeo de tipos para Haskell.

En el AG podemos borrar los casos erroneos de los expresiones case, sin afectar los resultados. Y para una mejor presentacion y cuidado, podemos exportar junto con el modulo, solo las funciones wrappers y tipos necesarios.

Bueno, esta es la manera en que implemente Phantom Types con AGs. Si ves el documento fuente, te preguntaras como implemetar GADTs con AGs, pues es algo que yo aun sigo preguntandome, y la verdad no se si habra una manera de hacerlo con AGs, pero como al autor de decia, Phantom Types es una solucion elegante, relativamente sencilla y estandard de Haskell en comparacion con GADTs.