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:
, 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 String
s:
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: