Haskell
En esta sección se presentan distintas propiedades del lenguaje Haskell, basado en el paradigma de programación funcional.
Tipos
Haskell es un lenguaje de programación de tipado fuerte y estático. Esto significa que:
1 ) Toda expresión tiene un tipo definido y sin cambiar.
2 ) Los tipos son verificados antes de ejecutar (al compilar) el programa.
Por ejemplo, veamos la siguiente función:
add :: Int -> Int -> IntSi intentamos ejecutar la función add con: add 1 2.0, Haskell nos arrojará un error de tipo, ya que el segundo argumento es de tipo Float y no de tipo Int.
Haskell puede inferir el tipo de una función sin necesidad de declararlo explícitamente. Por ejemplo, la función add puede ser definida de la siguiente manera:
add x y = x + yHaskell inferirá que x e y son de tipo Num a => a, donde Num es una clase de tipo que incluye a todos los números, y a es una variable de tipo. Esto se puede consultar con el siguiente comando:
:t add
add :: Num a => a -> a -> aAlgunos de los tipos son básicos, como Int, Float, Double, Bool, Char, entre otros. También existen tipos compuestos, como las listas y tuplas vistas en IAA.
Creando Listas
Existen algunas formas distintas de crear listas en Haskell:
1) Por extensión:
lista = [1, 2, 3, 4, 5]2) Por rango:
lista = [1..n]
-- ghci> lista
-- [1, 2, 3, 4, n]En este caso se usa la notación .. para definir un rango de valores.
3) Por repetición:
lista = replicate 5 1
-- ghci> lista
-- [1, 1, 1, 1, 1]replicate toma un número n y un valor x, y devuelve una lista con n elementos iguales a x.
4) Por comprensión:
lista = [x | x <- [1..5]]
listaConTuplas = [(x, y) | x <- [1..3], y <- [1..3]]
-- ghci> lista
-- [1, 2, 3, 4, 5]
-- ghci> listaConTuplas
-- [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)]En este caso, x toma los valores de la lista [1..3] e y toma los valores de la lista [1..3].
La notación | se lee como “tal que”, y se usa para determinar los valores de x e y que cumplen con la condición (similar a definir un conjunto por comprensión en matemática).
5) Por concatenación:
lista = [1] ++ [2] ++ [3] ++ [4] ++ [5]
-- ghci> lista
-- [1, 2, 3, 4, 5]Polimorfismo
Haskell soporta polimorfismo, lo que significa que las expreisones pueden tener más de un tipo. Existen dos tipos de polimorfismo: paramétrico y ad-hoc.
Polimorfismo paramétrico
El polimorfismo paramétrico es aquel en el que una función puede ser aplicada a cualquier tipo de datos. Por ejemplo, la función
idque devuelve el mismo valor que recibe:
id :: a -> a
id x = x
-- ghci> id 1
-- 1Donde a es una variable de tipo reemplazable por cualquier tipo de dato (ej. Int, Float, String, etc.).
Si tuviesemos la función definida:
f x = x + 1Haskell inferirá que x es de tipo Num a => a, ya que el operador + requiere que x sea un número.
Polimorfismo ad-hoc
El polimorfismo ad-hoc se refiere a la capacidad de una función de tener distintos comportamientos según el tipo de datos a usar. Por ejemplo, la función
elem:
elem :: Eq a => a -> [a] -> BoolDonde Eq es una typeclass que incluye a todos los tipos que pueden ser comparados por igualdad. Las typeclases son como interfaces en otros lenguajes de programación.
Si hubiesemos definido la función elem de la siguiente manera:
elem :: a -> [a] -> BoolHaskell nos arrojaría un error de tipo, ya que el operador == requiere que a sea de tipo Eq.
Existen otras typeclasses en Haskell, como Ord (para tipos ordenables), Show (para tipos que pueden ser convertidos a String), Num (para tipos numéricos), entre otros.
Currificación
En Haskell, todas las funciones son currificadas. Esto significa que una función que recibe múltiples argumentos, en realidad recibe un solo argumento y devuelve otra función que recibe el siguiente argumento.
Tomemos por ejemplo la función max:
max :: Ord a => a -> a -> aEn primera instancia podemos pensar que la función max recibe dos argumentos, pero en realidad recibe un solo argumento y devuelve otra función que recibe el siguiente argumento y lo compara.
Por ejemplo, si ejecutamos max 1 2, Haskell lo interpretará como (max 1) 2.
Sería como definir:
max :: Ord a => a -> (a -> a)Aplicación parcial
La aplicación parcial es una técnica que consiste en aplicar una función a menos argumentos de los que espera. Por ejemplo, si tenemos la función
add:
add :: Int -> Int -> Int
add x y = x + y
-- ghci> add 1 2
-- 3Podemos aplicar la función add a un solo argumento, y obtener otra función que recibe el siguiente argumento:
(add 1) :: Int -> Int
-- ghci> (add 1) 2
-- 3 Funciones de alto orden
Las funciones de alto orden son aquellas que toman funciones como argumentos y/o devuelven funciones como resultado. Por ejemplo, la función:
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
-- ghci> applyTwice (+2) 1
-- 5Donde applyTwice toma una función como argumento (también llamado callback) y un valor, y aplica la función dos veces al valor.
Haskell posee funciones de alto orden como map, filter, entre otras.
Funciones Lambda o Anónimas
Las funciones anónimas o lambda son funciones que no tienen un nombre asociado. Por ejemplo, la función
applyTwicepuede ser definida de la siguiente manera:
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
-- ghci> applyTwice (\x -> x + 2) 1
-- 5Por ejemplo podríamos definir una función que sume 3 a los elementos de una lista:
sumarTres :: [Int] -> [Int]
sumarTres xs = map (\x -> x + 3) xs
-- ghci> sumarTres [1, 2, 3, 4, 5]
-- [4, 5, 6, 7, 8]Usamos \ para definir una función lambda, y -> para separar los argumentos de la función del cuerpo de la función. Esta sintáxis corta es útil para funciones simples que no necesitan un nombre.
Tipos algebraicos sin parámetros
Si bien Haskell ofrece tipos de datos básicos y compuestos, también es posible definir nuevos tipos de datos.
Supongamos que definimos una función que enuncia nuestra tarea según el día de la semana:
tareaDiaria :: Int -> String
tareaDiaria 0 = "Trabajar"
tareaDiaria 1 = "Trabajar"
tareaDiaria 2 = "Trabajar"
... (En este ejemplo el 0 representa el lunes). Esto resulta difícil de leer y mantener. La codificación ad hoc puede funcionar, pero requiere tener mucho más cuidado.
En su lugar, podemos definir un nuevo tipo de datos Dia:
data Dia = Lunes | Martes | Miercoles | Jueves | Viernes | Sabado | Domingo
tareaDiaria :: Dia -> String
tareaDiaria Lunes = "Trabajar"
tareaDiaria Martes = "Trabajar"
tareaDiaria Miercoles = "Trabajar"
tareaDiaria Jueves = "Trabajar poco"
tareaDiaria Viernes = "Trabajar poco"
tareaDiaria Sabado = "Descansar"
tareaDiaria Domingo = "Descansar"
tareaDiaria _ = "Trabajar"Donde _ representa un comodín que captura cualquier otro valor que no esté definido en la función.
Incluso podríamos mejorar el anterior ejemplo definiendo un nuevo tipo de datos Tarea:
data Dia = Lunes | Martes | Miercoles | Jueves | Viernes | Sabado | Domingo
data Tarea = Trabajar | Descansar | TrabajarPoco
tareaDiaria :: Dia -> Tarea
tareaDiaria Lunes = Trabajar
tareaDiaria Martes = Trabajar
tareaDiaria Miercoles = Trabajar
tareaDiaria Jueves = TrabajarPoco
tareaDiaria Viernes = TrabajarPoco
tareaDiaria Sabado = Descansar
tareaDiaria Domingo = Descansar
tareaDiaria _ = Trabajar
-- ghci> tareaDiaria Lunes
-- TrabajarSintaxis case
Siguiendo el ejemplo anterior, si queremos definir una función horasTrabajo que devuelva la cantidad de horas de trabajo según nuestra tarea diaria, podríamos hacerlo:
horasTrabajo :: Dia -> Int
horasTrabajo d = case tareaDiaria d of
Trabajar -> 8
TrabajarPoco -> 4
_ -> 0Donde case es una expresión que permite hacer pattern matching sobre una expresión. En este caso usamos los valores del tipo Tarea para asignar una cantidad de horas según el día (valor).
(Esto es similar a un switch en otros lenguajes de programación).
Instancias derivadas y definiciones locales
Siguiendo lo anterior, no podríamos haber definido horasTrabajo de la siguiente manera:
horasTrabajo :: Dia -> Int
horasTrabajo d | tareaDiaria d == Trabajar = 8
| tareaDiaria d == TrabajarPoco = 4
| otherwise = 0
-- ghci> horasTrabajo Lunes
-- Error: ... (Podríamos hacerlo con where):
horasTrabajo :: Dia -> Int
horasTrabajo d | tarea d == Trabajar = 8
| tarea d == TrabajarPoco = 4
| otherwise = 0
where tarea = tareaDiaria dSin embargo, esto no permite comparar directamente dos valores de tipo Tarea con ==, ya que Haskell no sabe cómo compararlos.
Para ello, el tipo Tarea debe ser una instancia de la typeclass Eq:
data Tarea = Trabajar | Descansar | TrabajarPoco deriving Eq
horasTrabajo :: Dia -> Int
horasTrabajo d | tareaDiaria d == Trabajar = 8
| tareaDiaria d == TrabajarPoco = 4
| otherwise = 0
-- ghci> horasTrabajo Lunes
-- 8De esta forma, deriving nos permite derivar instancias de typeclasses para nuestros tipos de datos.
Tarea ahora es una instancia de Eq, por lo que podemos comparar valores de tipo Tarea con ==.
Tipos algebraicos con parámetros
Los tipos algebraicos pueden tener parámetros, lo que permite definir tipos de datos más genéricos.
Supongamos que tenemos el siguiente tipo de dato Figura:
data Figura = Circulo | RectanguloLo que nos dice este tipo de dato es que una Figura puede ser un Circulo o un Rectangulo. Ambos son constructores de datos, pero no tienen parámetros.
Si quisieramos diferenciar los tamaños de superficie en Circulo y Rectangulo, una forma podría ser la siguiente:
data Figura = CirculoChico
| CirculoMediano
| CirculoGrande
| RectanguloChico
| RectanguloMediano
| RectanguloGrandeSin embargo, esto implica que debemos definir cada tipo de figura con su tamaño, lo que resulta tedioso (e impreciso).
Existe una forma mucho mejor de hacerlo, y de poder adoptar cualquier dimensión para cada figura:
data Figura = Circulo (Float, Float) Float | Rectangulo (Float, Float) (Float, Float)
:t Circulo
- Circulo :: (Float, Float) -> Float -> Figura
:t Rectangulo
- Rectangulo :: (Float, Float) -> (Float, Float) -> FiguraAhora Circulo y Rectangulo son constructores de datos que reciben parámetros.
Para que Circulo y Rectangulo sean instancias válidas de Figura, es necesario que cumplan con los parámetros definidos.
En el ejemplo Circulo recibe un par de coordenadas y un radio, mientras que Rectangulo recibe dos pares de coordenadas.
Ahora podemos definir una función que nos diga el área de cada figura:
area :: Figura -> Float
area (Circulo (x, y) r) = 3.1416 * r * r
area (Rectangulo (x1, y1) (x2, y2)) = base * altura
where base = abs (x2 - x1)
altura = abs (y2 - y1)
-- ghci> area (Circulo (0, 0) 2)
-- 12.5664
-- ghci> area (Rectangulo (0, 0) (2, 2))
-- 4.0Los identificadores x, y, r, x1, y1, x2 e y2 son cualquier valor de tipo Float.
Sinónimos de tipos
Los sinónimos de tipos son una forma de darle un nombre alternativo a un tipo existente. Por ejemplo, si quisieramos definir un tipo
PuntooRadio:
type Punto = (Float, Float)
type Radio = Float
data Figura = Circulo Punto Radio | Rectangulo Punto Punto
:t Circulo
- Circulo :: Punto -> Radio -> Figura
:t Rectangulo
- Rectangulo :: Punto -> Punto -> Figura
-- ghci> area (Circulo (0, 0) 2)
-- 12.5664
-- ghci> area (Rectangulo (0, 0) (2, 2))
-- 4.0La función que definimos anteriormente sigue funcionando, y ahora Punto y Radio son sinónimos de Float, Float y Float, respectivamente.
Definición de Igualdad
Es posible definir la igualdad entre dos valores de un tipo de dato específico mediante la instancia de la typeclass
Eq.
instance Eq Figura
where
f1 == f2 = area f1 == area f2Donde f1 y f2 son dos figuras, y area es la función que definimos anteriormente.
Tipos recursivos
Un tipo recursivo es aquel que se define en términos de sí mismo.
Por ejemplo, si quisieramos representar una palabra:
data Palabra = PVacia | Agregar Char PalabraDonde PVacia representa una palabra vacía, y Agregar agrega un caracter a la palabra. Por ejemplo, la palabra “Hola” se representaría como:
let p = Agregar 'H' (Agregar 'o' (Agregar 'l' (Agregar 'a' PVacia)))Para poder imprimir la palabra, necesitamos definir una función que recorra la palabra y la imprima:
imprimir :: Palabra -> String
imprimir PVacia = ""
imprimir (Agregar c p) = c : imprimir p
-- ghci> imprimir p
-- "Hola"Tipo Maybe
El tipo
Maybees un tipo de dato que puede tener un valor o no tenerlo.
data Maybe a = Nothing | Just aEl tipo Maybe agrega un elemento Nothing que representa la ausencia de un valor, y un elemento Just a que representa un valor de tipo a.
Por ejemplo, si quisieramos definir una función que divida dos números:
dividir :: Float -> Float -> Maybe Float
dividir _ 0 = Nothing
dividir x y = Just (x / y)
-- ghci> dividir 1 0
-- Nothing
-- ghci> dividir 1 2
-- Just 0.5Si el divisor es 0, la función devuelve Nothing, ya que su resultado no existe en el tipo de la imagen de la función.
Podriamos saber si la división entre dos números es posible mediante el uso de Maybe y case:
dividir :: Float -> Float -> Maybe Float
dividir _ 0 = Nothing
dividir x y = Just (x / y)
puedeDividirse :: Float -> Float -> String
puedeDividirse x y = case dividir x y of
Nothing -> "No se puede dividir"
Just z -> "Se puede dividir"
-- ghci> puedeDividirse 1 0
-- "No se puede dividir"
-- ghci> puedeDividirse 1 2
-- "Se puede dividir"