Disclaimer: Como en la mayoría de mis posts, hay que tener conocimientos previos de programación para ver esto. Si no los tienes, posiblemente no te enteres de todo.
Hay conceptos que son algo más sencillos pero para otras cosas hay que tirar de conceptos no tan básicos y a veces tiro de Java para explicar algunas cosillas.
Si después de este disclaimer sigues teniendo ganas de aprender Haskell, pues adelante, aquí tienes una pequeña intro, disfruta.
¿Qué es Haskell?
Simplemente un lenguaje no funcional puro.
Esto significa, que es distinto a la mayoría de lenguajes que habréis visto (imperativos), como serían Java, C++ y del estilo.
Es un tipo de lenguaje basado en cómputos a través de funcions matemáticas (o puras).
Podemos decir que -> Haskell = Puro + Tipado estático fuerte + Perezoso
* Puro: los datos son inmutables (como las String de Java)
* Tipado estático fuerte:
- el tipo se establece en tiempo de compilación y no varía
- no se pueden mezclar tipos incompatibles
- no hay conversión implícita entre tipos (ni siquiera entre números)
* Perezoso: sólo se evalúa lo necesario para obtener el resultado
Se usa la notación currificada, que es algo diferente a la notación matemática típica, en la que los argumentos de una función se encierran entre paréntesis y se separan por comas, o que la multiplicación se escriba como la yuxtaposición de dos factores sin ningún operador. Ejemplo : f(a, b) + cd
En la notación currificada, se denota la multiplicación con un "*" y los argumentos de una función se denotan yuxtaponiendo los argumentos sin paréntesis ni comas.
Algunos ejemplos de currificación serían:
g(x)+y ----- > g x + y
f(x)*g(x) ----- > f x * g x
f(x,y) + u*v ----- > f x y + u*v
max(max(x,y+1), max(z,-z)) ----- > max ( max x (y+1) max z (-z))
Ahora, la definición de funciones. En Java, las definirías algo así:
Pues en Haskell sería así:
Quedando la sintaxis algo tipo nombreFunción :: Tipo (el que se toma) -> Tipo (el que devuelve)
Pudiendo poner si quiero nombreFuncion :: Tipo (el que se toma) -> Tipo (el que se toma) -> Tipo (el que se toma) -> Tipo (el que se toma) -> ... -> Tipo (el que se devuelve)
Nota: Los comentarios se hacen con "--" para una línea o para varias líneas poniendo "{-" para abrir y "-}" para cerrar.
Ejemplos de funciones en Haskell:
Como se ve en las funciones de arriba, Haskell también tiene if/else pero con un then. Es decir, quedaría como if algo se cumple then pasa x else pasa y
Importante:
1. El if then else en Haskell es una expresión, no una sentencia
2. El else es obligatorio
3. Los tipos de then y else deben coincidir
Además de if/else, Haskell también soporta guardas (similar a las funciones por casos). A partir de 2 casos es mucho mejor el uso de guardas que anidar if/elses. Ejemplo:
Haskell distingue mayúsculas de minúsculas (case sensitive).
Los identificadores (nombres) de funciones y argumentos, deben empezar con una letra minúscula o el carácter _ (underscore) y éste puede ir seguido de letras (mayúsculas o minúsculas), dígitos, tildes (') o "_"
- Ejemplos válidos de identificadores: "f" "f'" "f2" "x" "_uno" "factorial" "árbol" "toList" "p1_gcd".
- Ejemplos no válidos: "Function" "2f" "f!y" Los nombres de tipos deben empezar con una letra mayúsculaInt, Integer, Bool, Float, Double, etc
Los nombres de tipos deben empezar con una letra mayúscula: Int, Integer, Bool, Float, Double, etc.
Haskell también tiene recursividad, algo muy importante y que para más adelante se va a usar MUCHO. En resumen, haskell sin recurisividad no es nada.
Un ejemplo sería la función factorial, que lo puedes poner con if then else, con guardas o al gusto:
Dualidad función/operador:
Una función binaria (div, max, ...) puede usarse como operador (infijo) si escribimos su nombre entre comillas simples invertidas (a la derecha de la tecla p)
div x y == x `div` y
max x y == x `max` y
(siendo div y max funciones ya predefinidas en Haskell)
Un operador binario (+, *, ...) se puede usar como función (prefijo) si escribimos su nombre entre parántesis
x + y == (+) x y
Ejemplos de ambos casos:
El operador unario "-" es el único operador unario simbólico prefijo:
-1
-(1+2) -> -3
El resto de operadores simbólicos son binarios y se utilizan de forma infija:
1+2 -> 3
1 + 2*3 -> 7
1 + -2 -> error
1 + (-2) -> -1
Asociatividad:
Esta característica permite resolver la ambiguedad en ausencia de paréntesis.
Tuplas:
El tipo tupla en Haskell:
- las tuplas son como vectores
- un valor de tipo tupla se escribe encerrando entre paréntesis sus componentes, separados entre sí por comas:
(exp_1, exp_2, ..., exp_n)
- el número de componentes es fijo, n
- cada componente exp_i puede ser de un tipo distinto
- es un producto cartesiano
- existe la tupla vacía: ()
- no existe la tupla unitaria: (x)
Ejemplos de tuplas:
Una vez compilado, poniendo en terminial "unaTupla" y "tuplaAnidada" se vería algo tipo:
Por lo que todo va perfe.
Utilidad de las tuplas: una función puede devolver varios resultados
Ejemplos:
>>> sucPred 5
(6,4)
Functiones monomórficas:
Una función es monomórfica si los tipos de todos sus argumentos y el tipo de su resultado son concretos (empiezan por mayúscula).
Todas las funciones que hemos visto hasta el momento son monomórficas. Por ahora nada nuevo, pero hay que definir las cosas por su nombre.
Funciones polimórficas:
Esto puede ser algo confuso, si has tocado Java por ejemplo ya que, lo que en Java se "genericidad", en Haskell se llama "polimorfismo". Así que si hablamos de polimorfismo, no hablamos del polimorfismo de Java.
En Java podemos introducir un tipo genérico mediante <T>:
donde T puede ser cualquier tipo Java.
Una función es polimórfica si el tipo de al menos uno de sus argumentos o el tipo de su resultado no son concretos, sino que puede ser cualquier tipo Haskell.
Antes por ejemplo definíamos funciones de enteros a enteros (Int -> Int), ahora pueden ser de cualquier tipo a cualquier tipo.
En Haskell si un tipo empieza por minúscula es una variable de tipo.
Suelen utilizarse las primeras letras del alfabeto: a, b, c, ...
identidad :: a -> a
identidad x = x
Por ejemplo, con funciones monomórficas haríamos lo siguiente:
-- >>> primeroI (6,8)
-- 6
Que sólo permitiría el paso a enteros (si metemos un char por ejemplo, me pega un castañazo). Mientras que con polimorfismo, podemos hacer:
Funciones sobrecargadas:
En Java puedo restringir un tipo genérico:
T extends Comparable<T>
En Haskell puedo restringir una variable de tipo:
Ord a => a
Ord es una clase de Haskell semejante a la interfaz Comparable de Java.
En Haskell no todos los tipos tienen definida igualdad, mientras que en Java si, no hay que confundirse.
"Eq" a y la flecha "=>" significa que aseguramos que los tipos de a, tengan incluida la operacion igualdad, así puedo igualar x==u y tambien y == v
En sonSimétricos ambos componentes de la tupla tienen el mismo tipo, y en el sonSimétricos' pueden tener tipos distintos y así se denota la sintaxis como señalé antes.
Funciones parciales:
Una función se dice parcial si no está definida para algún valor de sus argumentos.
Una función se dice total si está definida para todos los posibles valores de sus argumentos.
Si una función es parcial, podemos elevar una excepción cuando no esté definida.
Definiciones locales:
Por ejemplo, hagamos el cálculo de las raíces de una ecuación de segundo grado: ax^2 + bx + c = 0
La siguiente definición repite cálculos y no es muy legible:
Podemos mejorar la modularidad, la legilidad y la eficiencia introduciendo definiciones locales con "where" para aquellas expresiones que se repiten.
Ejemplos de resultados:
>>> raíces 2 7 1
(-0.14921894064178787,-3.350781059358212)
>>> raíces 2 4 2
(-1.0,-1.0)
>>> raíces 2 0 1
*** Exception: raíces: las raíces son complejas
>>> raíces 0 1 1
*** Exception: raíces: la ecuación no es de segundo grado
La regla del sangrado:
En muchos lenguajes de programación se usan llaves {} para delimitar bloques (cuerpos de función, bucles, etc.).
En Haskell no se usan llaves: el sangrado es significativo y determina la estructura del código.
Una definición termina cuando se encuentra código que tiene el mismo o menos sangrado (o cuando se acabe el fichero).
En resumen: si no pones el sangrado como debes, te va a pegar el código un castañazo, como por ejemplo si pones las guardas sin sangrado.
Para acabar os dejo un último ejemplo de definiciones locales que se ve mejor que el otro creo yo:
Por ahora esto es la parte 1. Es una intro para que os hagais una idea de qué coño es este lenguaje raro. En la parte 2 ya entran conocimientos más avanzados, y en la 3 más avanzados y así, por lo que si te interesa este contenido estate ready.
Hay conceptos que son algo más sencillos pero para otras cosas hay que tirar de conceptos no tan básicos y a veces tiro de Java para explicar algunas cosillas.
Si después de este disclaimer sigues teniendo ganas de aprender Haskell, pues adelante, aquí tienes una pequeña intro, disfruta.
¿Qué es Haskell?
Simplemente un lenguaje no funcional puro.
Esto significa, que es distinto a la mayoría de lenguajes que habréis visto (imperativos), como serían Java, C++ y del estilo.
Es un tipo de lenguaje basado en cómputos a través de funcions matemáticas (o puras).
Podemos decir que -> Haskell = Puro + Tipado estático fuerte + Perezoso
* Puro: los datos son inmutables (como las String de Java)
* Tipado estático fuerte:
- el tipo se establece en tiempo de compilación y no varía
- no se pueden mezclar tipos incompatibles
- no hay conversión implícita entre tipos (ni siquiera entre números)
* Perezoso: sólo se evalúa lo necesario para obtener el resultado
Se usa la notación currificada, que es algo diferente a la notación matemática típica, en la que los argumentos de una función se encierran entre paréntesis y se separan por comas, o que la multiplicación se escriba como la yuxtaposición de dos factores sin ningún operador. Ejemplo : f(a, b) + cd
En la notación currificada, se denota la multiplicación con un "*" y los argumentos de una función se denotan yuxtaponiendo los argumentos sin paréntesis ni comas.
Algunos ejemplos de currificación serían:
g(x)+y ----- > g x + y
f(x)*g(x) ----- > f x * g x
f(x,y) + u*v ----- > f x y + u*v
max(max(x,y+1), max(z,-z)) ----- > max ( max x (y+1) max z (-z))
Ahora, la definición de funciones. En Java, las definirías algo así:
Java:
int square(int x) {
return x * x;
}
Código:
square :: Integer -> Integer
square x = x * x
Quedando la sintaxis algo tipo nombreFunción :: Tipo (el que se toma) -> Tipo (el que devuelve)
Pudiendo poner si quiero nombreFuncion :: Tipo (el que se toma) -> Tipo (el que se toma) -> Tipo (el que se toma) -> Tipo (el que se toma) -> ... -> Tipo (el que se devuelve)
Nota: Los comentarios se hacen con "--" para una línea o para varias líneas poniendo "{-" para abrir y "-}" para cerrar.
Ejemplos de funciones en Haskell:
Código:
máximo :: Integer -> Integer -> Integer -- predefinida como max
máximo x y = if x >= y then x else y
máximoDeTres :: Integer -> Integer -> Integer -> Integer
máximoDeTres x y z = máximo x (máximo y z)
signo :: Integer -> Integer -- predefinida como signum
signo x = if x > 0 then 1
else if x < 0 then -1
else 0
Como se ve en las funciones de arriba, Haskell también tiene if/else pero con un then. Es decir, quedaría como if algo se cumple then pasa x else pasa y
Importante:
1. El if then else en Haskell es una expresión, no una sentencia
2. El else es obligatorio
3. Los tipos de then y else deben coincidir
Además de if/else, Haskell también soporta guardas (similar a las funciones por casos). A partir de 2 casos es mucho mejor el uso de guardas que anidar if/elses. Ejemplo:
Código:
máximoDeTres' :: Integer -> Integer -> Integer -> Integer
máximoDeTres'
| x >= y && x >= z = x
| y >= z = y
| otherwise = z
Haskell distingue mayúsculas de minúsculas (case sensitive).
Los identificadores (nombres) de funciones y argumentos, deben empezar con una letra minúscula o el carácter _ (underscore) y éste puede ir seguido de letras (mayúsculas o minúsculas), dígitos, tildes (') o "_"
- Ejemplos válidos de identificadores: "f" "f'" "f2" "x" "_uno" "factorial" "árbol" "toList" "p1_gcd".
- Ejemplos no válidos: "Function" "2f" "f!y" Los nombres de tipos deben empezar con una letra mayúsculaInt, Integer, Bool, Float, Double, etc
Los nombres de tipos deben empezar con una letra mayúscula: Int, Integer, Bool, Float, Double, etc.
Haskell también tiene recursividad, algo muy importante y que para más adelante se va a usar MUCHO. En resumen, haskell sin recurisividad no es nada.
Un ejemplo sería la función factorial, que lo puedes poner con if then else, con guardas o al gusto:
Código:
factorial :: Integer -> Integer
factorial x = if x == 0 then 1 --caso base
else x * factorial (x - 1) --caso recursivo
factorial' :: Integer -> Integer
factorial' x
| x == 0 = 1 -- CB
| otherwise = x * factorial (x-1) -- CR
Dualidad función/operador:
Una función binaria (div, max, ...) puede usarse como operador (infijo) si escribimos su nombre entre comillas simples invertidas (a la derecha de la tecla p)
div x y == x `div` y
max x y == x `max` y
(siendo div y max funciones ya predefinidas en Haskell)
Un operador binario (+, *, ...) se puede usar como función (prefijo) si escribimos su nombre entre parántesis
x + y == (+) x y
Ejemplos de ambos casos:
Código:
máximoDeTres'' :: Int -> Int -> Int -> Int
máximoDeTres'' x y z = x `max` y `max` z
suma :: Int -> Int -> Int
suma x y = (+) x y
El operador unario "-" es el único operador unario simbólico prefijo:
-1
-(1+2) -> -3
El resto de operadores simbólicos son binarios y se utilizan de forma infija:
1+2 -> 3
1 + 2*3 -> 7
1 + -2 -> error
1 + (-2) -> -1
Asociatividad:
Esta característica permite resolver la ambiguedad en ausencia de paréntesis.
Tuplas:
El tipo tupla en Haskell:
- las tuplas son como vectores
- un valor de tipo tupla se escribe encerrando entre paréntesis sus componentes, separados entre sí por comas:
(exp_1, exp_2, ..., exp_n)
- el número de componentes es fijo, n
- cada componente exp_i puede ser de un tipo distinto
- es un producto cartesiano
- existe la tupla vacía: ()
- no existe la tupla unitaria: (x)
Ejemplos de tuplas:
Código:
unaTupla :: (Bool, Int, Char, Double)
unaTupla = (True, 2 + 3, 'a', 3.1416)
tuplaAnidada :: (Int, (Char, Int))
tuplaAnidada = (1, ('A', 65))
Una vez compilado, poniendo en terminial "unaTupla" y "tuplaAnidada" se vería algo tipo:
Por lo que todo va perfe.
Utilidad de las tuplas: una función puede devolver varios resultados
Ejemplos:
Código:
sucPred :: Integer -> (Integer, Integer)
sucPred x = (x-1, x+1)
>>> sucPred 5
(6,4)
Functiones monomórficas:
Una función es monomórfica si los tipos de todos sus argumentos y el tipo de su resultado son concretos (empiezan por mayúscula).
Todas las funciones que hemos visto hasta el momento son monomórficas. Por ahora nada nuevo, pero hay que definir las cosas por su nombre.
Funciones polimórficas:
Esto puede ser algo confuso, si has tocado Java por ejemplo ya que, lo que en Java se "genericidad", en Haskell se llama "polimorfismo". Así que si hablamos de polimorfismo, no hablamos del polimorfismo de Java.
En Java podemos introducir un tipo genérico mediante <T>:
Java:
public static <T> T identidad(T x) {
return x;
}
donde T puede ser cualquier tipo Java.
Una función es polimórfica si el tipo de al menos uno de sus argumentos o el tipo de su resultado no son concretos, sino que puede ser cualquier tipo Haskell.
Antes por ejemplo definíamos funciones de enteros a enteros (Int -> Int), ahora pueden ser de cualquier tipo a cualquier tipo.
En Haskell si un tipo empieza por minúscula es una variable de tipo.
Suelen utilizarse las primeras letras del alfabeto: a, b, c, ...
identidad :: a -> a
identidad x = x
Por ejemplo, con funciones monomórficas haríamos lo siguiente:
Código:
primeroI :: (Integer, Integer) -> Integer
primeroI (x,y) = x
-- >>> primeroI (6,8)
-- 6
Que sólo permitiría el paso a enteros (si metemos un char por ejemplo, me pega un castañazo). Mientras que con polimorfismo, podemos hacer:
Código:
primero :: (a,b) -> a -- predefinida como fst
primero (x, y) = x
Funciones sobrecargadas:
En Java puedo restringir un tipo genérico:
T extends Comparable<T>
En Haskell puedo restringir una variable de tipo:
Ord a => a
Ord es una clase de Haskell semejante a la interfaz Comparable de Java.
Código:
iguales :: (Eq a, Eq b) =>(a, b) -> (a, b) -> Bool
iguales (x, y) (u, v) = x == u && y == v
sonSimétricos :: Eq a =>(a, a) -> (a, a) -> Bool
sonSimétricos (x, y) (u, v) = x == v && y == u
sonSimétricos' :: (Eq a, Eq b) =>(a, b) -> (b, a) -> Bool
sonSimétricos' (x, y) (u, v) = x == v && y == u
En Haskell no todos los tipos tienen definida igualdad, mientras que en Java si, no hay que confundirse.
"Eq" a y la flecha "=>" significa que aseguramos que los tipos de a, tengan incluida la operacion igualdad, así puedo igualar x==u y tambien y == v
En sonSimétricos ambos componentes de la tupla tienen el mismo tipo, y en el sonSimétricos' pueden tener tipos distintos y así se denota la sintaxis como señalé antes.
Funciones parciales:
Una función se dice parcial si no está definida para algún valor de sus argumentos.
Una función se dice total si está definida para todos los posibles valores de sus argumentos.
Si una función es parcial, podemos elevar una excepción cuando no esté definida.
Código:
inverso :: Double -> Double -- predefinida como recip
inverso x
| x == 0 = error "Inverso: DIVISION POR CERO NO SE PUEDE"
| otherwise = 1/x
Definiciones locales:
Por ejemplo, hagamos el cálculo de las raíces de una ecuación de segundo grado: ax^2 + bx + c = 0
La siguiente definición repite cálculos y no es muy legible:
Código:
raíces :: Double -> Double -> Double -> (Double, Double)
raíces a b c
| a == 0 = error "raíces: la ecuación no es de segundo grado"
| b*b-4*a*c < 0 = error "raíces: las raíces son complejas"
| otherwise = ((-b + sqrt (b*b-4*a*c)) / (2*a),
(-b - sqrt (b*b-4*a*c)) / (2*a))
Podemos mejorar la modularidad, la legilidad y la eficiencia introduciendo definiciones locales con "where" para aquellas expresiones que se repiten.
Código:
raíces' :: Double -> Double -> Double -> (Double, Double)
raíces' a b c
| a == 0 = error "raíces': la ecuación no es de segundo grado"
| disc < 0 = error "raíces': las raíces son complejas"
| otherwise = ((-b + raizDisc) / dosA,
(-b - raizDisc) / dosA)
where
disc = b*b - 4*a*c
raizDisc = sqrt disc
dosA = 2*a
Ejemplos de resultados:
>>> raíces 2 7 1
(-0.14921894064178787,-3.350781059358212)
>>> raíces 2 4 2
(-1.0,-1.0)
>>> raíces 2 0 1
*** Exception: raíces: las raíces son complejas
>>> raíces 0 1 1
*** Exception: raíces: la ecuación no es de segundo grado
La regla del sangrado:
En muchos lenguajes de programación se usan llaves {} para delimitar bloques (cuerpos de función, bucles, etc.).
En Haskell no se usan llaves: el sangrado es significativo y determina la estructura del código.
Una definición termina cuando se encuentra código que tiene el mismo o menos sangrado (o cuando se acabe el fichero).
En resumen: si no pones el sangrado como debes, te va a pegar el código un castañazo, como por ejemplo si pones las guardas sin sangrado.
Para acabar os dejo un último ejemplo de definiciones locales que se ve mejor que el otro creo yo:
Código:
circArea :: Double -> Double
circArea r = pi*r^2
rectArea :: Double -> Double -> Double
rectArea b h = b*h
circLength :: Double -> Double
circLength r = 2*pi*r
cylinderArea :: Double -> Double -> Double
cylinderArea r h = 2*circ + rect
where
circ = circArea r
l = circLength r
rect = rectArea l h
Por ahora esto es la parte 1. Es una intro para que os hagais una idea de qué coño es este lenguaje raro. En la parte 2 ya entran conocimientos más avanzados, y en la 3 más avanzados y así, por lo que si te interesa este contenido estate ready.
Última edición: