Clases de Tipos

As of March 2020, School of Haskell has been switched to read-only mode.

Ya hemos visto con buen detalle las funciones y los tipos en Haskell. En este tutorial veremos como expresar que para cierto tipo, existen ciertas funciones usando clases de tipos (type classes).

Motivación

Retomemos el último ejemplo del tutorial pasado:

data Human = FullName String String | Nickname String
simpleSalute (FullName firstName _) = "Hi, it's me, " ++ firstName ++ "!"
simpleSalute (Nickname nickname)    = "Hi, it's me, " ++ nickname ++ "!"

billGates = FullName "William" "Gates"
me        = Nickname "Lay"

main = do
  putStrLn (simpleSalute billGates)
  putStrLn (simpleSalute me)

Supongamos que también queremos modelar a los pokemones; los pokemones, según recuerdo, sólo pueden decir el nombre de su especie así que asumiremos que un pokemón saluda diciendo el nombre de su especie:

data Human   = FullName String String | Nickname String
data Pokemon = PokemonKind String

simpleSalute (FullName firstName _) = "Hi, it's me, " ++ firstName ++ "!"
simpleSalute (Nickname nickname)    = "Hi, it's me, " ++ nickname ++ "!"
simpleSalute (Pokemon pokemonKind) = pokemonKind

billGates = FullName "William" "Gates"
me        = Nickname "Lay"
pikachu   = Pokemon "Pikachu"

main = do
   putStrLn (simpleSalute billGates)
   putStrLn (simpleSalute me)
   putStrLn (simpleSalute pikachu)

Hasta ahora todo va mas o menos bien, pero no sabemos aún como implementar simpleSalute para pokemones. Si ejecutamos el programa nos dará un error diciendo que Human y Pokemon no hacen match; esto se debe a que Human y Pokemon son tipos completamente distintos y simpleSalute no puede tener simultáneamente los tipos Human -> String y Pokemon -> String.

Una alternativa es hacer Human y Pokemon parte de un tipo superior, ThingThatSalutes:

data Human = FullName String String | Nickname String
data Pokemon = PokemonKind String
data ThingThatSalutes = Human Human | Pokemon Pokemon

simpleSalute :: ThingThatSalutes -> String
simpleSalute (Human (FullName firstName _)) = "Hi, it's me, " ++ firstName ++ "!"
simpleSalute (Human (Nickname nickname))    = "Hi, it's me, " ++ nickname ++ "!"
simpleSalute (Pokemon (PokemonKind pokemonKind)) = pokemonKind ++ "!"

billGates = FullName "William" "Gates"
me        = Nickname "Lay"
pikachu   = PokemonKind "Pikachu"

main = do
   putStrLn (simpleSalute (Human billGates))
   putStrLn (simpleSalute (Human me))
   putStrLn (simpleSalute (Pokemon pikachu))

Y funciona, pero en mi opinión hay mucho "wrapping" ((simpleSalute (Human billGates)), (simpleSalute (Pokemon pikachu))) y el nombre del tipo ThingThatSalutes parece indicar que ya estamos creando abstracciones no muy buenas. Por fortuna, hay una alternativa, las clases de tipos (type classes).

Clases de tipos (Type classes)

Una clase de tipo es un mecanismo del sistema de tipos el cual nos permite definir clases de tipos. Una clase de tipo consiste en un nombre para la clase y las funciones que deben estar definidas para el tipo que quiera ser miembro de dicha clase. A continuación, definiremos una clase de tipo, Salutable, para resolver de manera más "elegante" el último ejemplo.

data Human = FullName String String | Nickname String
data Pokemon = PokemonKind String

-- Salutable's type class definition
{-hi-}class Salutable a where{-/hi-}
  simpleSalute :: a -> String
  
-- Human's implementation of the Salutable type class
{-hi-}instance Salutable Human where{-/hi-}
  simpleSalute (FullName firstName _) = "Hi, it's me, " ++ firstName ++ "!"
  simpleSalute (Nickname nickname)    = "Hi, it's me, " ++ nickname ++ "!"

-- Pokemon's implementation of the Salutable type class
{-hi-}instance Salutable Pokemon where{-/hi-}
  simpleSalute (PokemonKind p) = p ++ "!"

{-hi-}introduction x y ={-/hi-}
  do
    putStrLn (simpleSalute x)
    putStrLn (simpleSalute y)

billGates = FullName "William" "Gates"
me        = Nickname "Lay"
pikachu   = PokemonKind "Pikachu"

main = do
   introduction billGates me
   introduction billGates pikachu

Ahora que podemos hacer que un humano y un pokemon saluden, definamos una función introduction que recibe dos instancias de Salutable y regrese el saludo de cada uno. Para esto, necesitaremos usar las restricciones de tipo.

Restricciones de clase (class constraints)

Restricciones sobre los parámetros de una función

Si checamos el tipo de introduction en GHCi: introduction's type , podemos ver que el tipo de introduction inferido por el compilador es introduction :: (Salutable a, Salutable a1) => a -> a1 -> IO (); lo cual nos lleva a este tema, restricciones de clase (class constraints).

introduction :: (Salutable a, Salutable a1) => a -> a1 -> IO () significa que cualquier tipo que sustituya a la variable de tipo a debe de ser miembro de la clase Salutable; lo mismo para la variable de tipo a1. No es lo mismo a escribir introduction :: (Salutable a) => a -> a -> IO (), pues eso forzaría que ambos parámetros de introduction sean del mismo tipo (sea cual sea), lo cual no es la intención.

A introduction no le podemos pasar argumentos miembros de cualquier tipo; por ejemplo, no le podemos pasar dos Strings:

class Salutable a where
  simpleSalute :: a -> String

introduction x y =
  do
    putStrLn (simpleSalute x)
    putStrLn (simpleSalute y)

main = do
   {-hi-}introduction "Not a Salutable" "I'm a String"{-/hi-}

pues String no es miembro de la clase de tipo Salutable. A pesar de que nosotros no tuvimos que especificar el tipo de introduction, el compilador pudo inferir que sus argumentos pueden ser miembros de cualquier tipo siempre y cuando dichos tipos sean miembros de la clase de tipo Salutable. La inferencia es posible dado el uso de simpleSalute sobre los argumentos x y y en la función introduction; para que simpleSalute x y simpleSalute y hagan sentido, x y y deben de ser miembros de la clase Salutable.

Además de las restricciones de tipos sobre los parámetros de las funciones, existen otras formas de restricciones de tipos. A continuación veremos el resto de estas.

Restricciones de clase en definiciones de clases, implementaciones de clases y definiciones de constructores de tipos

Ahora que el concepto de restricciones de clase ha quedado claro, veremos la sintaxis que nos permitirá establecer restricciones en otras construcciones además de las definiciones de funciones.

Sintaxis básica de Haskell

Definiciones de clases de tipos
class (SomeExistingClass a) => ClassBeingDefined a1 where
someFunction :: a1 -> a -> a2
Implementaciones de clases
instance ClassBeingDefined TypeImplementingTheClass where
  someFunction (SomeExistingClass a1) => ClassBeingDefined -> SomeExistingClass -> a2
  someFunction memberOfTypeImplementingTheClass memberOfSomeExistingClass = ...
Definiciones de constructores de tipos

Algunos llaman a esto "herencia".

data (SomeExistingClass a) => SomeTypeConstructor a = SomeDataConstructor a

Ejemplo

A continuación, un ejemplo de todos los casos de restricciones de tipos (tomado de aquí). El ejemplo fue modificado para sólo utilizar los conceptos vistos hasta ahora.

-- Type synonyms
type PointName = String
type PointX = Int
type PointY = Int
type DeltaInX = Int
type DeltaInY = Int

-- Type constructors
data Position      = Position PointX PointY
data PositionDelta = PositionDelta DeltaInX DeltaInY
data NamedPoint    = NamedPoint PointName PointX PointY

-- Location, in two dimensions.
class Located a where
  getPosition :: a -> Position

class (Located a) => Movable a where
  setPosition :: a -> Position -> a

instance Located NamedPoint where
  getPosition (NamedPoint pointName x y) = Position x y

instance Movable NamedPoint where
  setPosition (NamedPoint pointName _ _) (Position x y)
    =
      NamedPoint pointName x y

-- Moves a value of a Movable type by the specified displacement.
-- This works for any movable, including NamedPoint.
move :: (Movable a) => a -> PositionDelta -> a
move p (PositionDelta dx dy) =
  setPosition p newPosition
  where
    Position x y = getPosition p
    newPosition = Position (x + dx) (y + dy)

showNamedPoint (NamedPoint pointName x y) =
  pointName ++ " is at (" ++ (show x) ++ ", " ++ (show y) ++ ")"

main =
  do
    putStrLn (showNamedPoint p)
    putStrLn (showNamedPoint p')

  where
    p = (NamedPoint "The point" 1 1)
    delta = PositionDelta 1 2
    p' = move p delta

Nuevamente, este ejemplo es sólo para fines pedagógicos. Se debe de tener mucho cuidado de no abusar de las clases de tipos. A continuación, algunos casos de éxito para las clases de tipos.

Clases de tipos básicas del Prelude

Eq

Eq define los operadores == y /= que se utilizan para comparar datos.

main =
  do
    putStrLn (show (1 == 2))
    putStrLn (show (1 /= 2))

Además de Int, otras instancias de Eq son:

Ord

Los tipos capaces de pertenecer a la clase Ord aquellos sobre los cuales puede existin un orden total. La clase Ord hereda de la clase [Eq]

class Eq a => Ord a where
...

y define las siguientes funciones y operaciones:

compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a

donde Ordering es una enumeración:

data Ordering =
    LT -- "less than"
  | GT -- "greater than"
  | EQ -- "equals"

Algunos tipos miembros de la clase Ord son: - Int - Integer - Float - Char - Number - Bool

Show

Show lo hemos usado en todos los tutoriales; se utiliza para obtener una representación de una estructura de datos en un String. La única función que define es show. Algunas instancias de Show son:

boolToStr :: Bool -> String
boolToStr bool = show bool

Num

Num define las siguientes funciones y operaciones:

(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a -- regresa el signo de un Num (+1 o -1)
fromInteger :: Integer -> a

Algunas de sus implementaciones son:

Ejercicios

Ejercicios

Soluciones

Siguiente tutorial

Siguiente tutorial