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