Hemos visto como definir funciones que dado un tipo nos retorne un tipo, definiendo constructores de tipo con la keyword type
.
Supongamos que queremos definir una función que dado un tipo nos retorne un valor.
Además nos gustaría poder hacer pattern matching sobre los tipos recibidos como parámetro.
Eso podría verse como lo siguiente
type Int = "int" :: Type -> String
String = "string"
Obviamente las lineas de arriba no son código Haskell válido, pero veremos como lograrlo.
Una typeclass es una interfaz que se le puede dar a un tipo para enriquecerlo.
En otras palabras, una typeclass sería equivalente una interface de java.
En java una _interface puede extender el conjunto de métodos que implementa una clase.
En Haskell una typeclass brinda un conjunto de funciones para el tipo dado.
Un ejemplo de esto es la typeclass Show
.
Al instanciar la typeclass Show
para un tipo particular T
se define una función ´show´, la cual dado un valor de tipo ´T´ retorna una representación en caracteres.
Ahora definiremos la typeclass TypeOf
.
Una instancia de esta typeclass definirá la función typeOf
, quien nos retornará un string indicando de que tipo es.
class TypeOf a where
typeOf :: a -> String
Ahora la instanciaremos para el tipo Int
, y daremos la implementación de typeOf
instance TypeOf Int where
typeOf :: Int -> String
typeOf n = "Int"
ghci> typeOf 5
"Int"
Notemos que el parámetro n
en la implementación de typeOf
no se está usando.
Esto se debe a que lo que nos interesa es el tipo del parámetro, y no su valor.
Lo habitual es no darles nombre y utilizar un _
, pero de todas formas debemos pasarle el parámetro (que será ignorado) al llamar a la función.
Damos más instancias de la typeclass para tener más codigo que analizar.
En el caso de la tupla, lo que está a la izquierda del =>
indica “es necesario que a
y b
tengan instancia de TypeOf
para que (a, b)
tenga instancia de TypeOf
”
instance TypeOf Char where
typeOf :: Char -> String
typeOf _ = "Char"
instance TypeOf String where
typeOf :: String -> String
typeOf _ = "String"
instance (TypeOf a, TypeOf b) => TypeOf (a, b) where
typeOf :: (a, b) -> String
typeOf (a, b) = "Tupla de " ++ typeOf a ++ " y " ++ typeOf b
Para continuar, si estamos escribiendo código en ghci debemos setear el flag :set -XAllowAmbiguousTypes
, o si estamos escribiendo en un archivo debe ser encabezado por el pragma {-# LANGUAGE AllowAmbiguousTypes #-}
Retomemos el punto, a typeOf
se le está pasando un valor como atributo que no está siendo utilizado.
Entonces podríamos redefinir la typeclass de la siguiente manera.
Notemos que no hay diferencia entre el typeOf
tipado para Int
del typeOf
tipado para String
, tampoco podemos marcar esa diferencia a través de parámetros.
La solución a esto es indicarle explicitamente a typeOf
cual instancia de TypeOf
debe utilizar, esto se logra utilizando el operador @
.
class TypeOf a where
typeOf :: String
instance TypeOf Int where
typeOf :: String
typeOf = "Int"
instance TypeOf String where
typeOf :: String
typeOf = "String"
ghci> typeOf
Ambiguous type variable ...
ghci> typeOf @Int
"Int"
Luego redefinimos las instancias de otros tipos.
Notemos que al hacer un uso recursivo de typeOf
utilizamos el mismo operador @
.
instance TypeOf Char where
typeOf :: String
typeOf = "Char"
instance TypeOf String where
typeOf :: String
typeOf = "String"
instance (TypeOf a, TypeOf b) => TypeOf (a, b) where
typeOf :: String
typeOf = "Tupla de " ++ typeOf @a ++ " y " ++ typeOf @b
Analicemos lo obtenido hasta ahora.
La función typeOf
no toma ningún parámetro.
Pero si ejecutamos typeOf @Int
nos retorna un string, un valor de tipo String
.
Más aún, podemos ejecutar typeOf @Char
, typeOf @String
o typeOf @(Int, Char)
y obtenemos distintos valores de tipo String
.
Si bien la función typeOf
no toma valores como parámetros, podemos mirarla como una función que dado un tipo nos retorna un valor.
Por lo tanto podemos mirar a nuestra función typeOf
como una función que va de tipos a términos, e incluso estamos haciendo pattern matching sobre los tipos.
Extra.
En calculo lambda no tipado podemos escribir abstracciones sencillas “dado un x, retorna el resultado de aplicar f a x” \ x . f x
.
Si vamos a calculo lambda tipado podemos escribir abstracciones como “dado un tipo t y dado un valor x de tipo t, retorna el resultado de aplicar f a x” \ t . \ (x : t) . f x
.
Si miramos el tipo de typeOf
, tiene cierta similitud a esta segunda expresión, solo que a
representa un tipo y k
representa un kind.
ghci> :t typeOf
typeOf :: forall {k} (a :: k). TypeOf a => String