Introducción a Haskell | Parte 1


LinceCrypt

Tu lince confiable
Noderador
Nodero
Noder
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í:
Java:
   int square(int x) {
      return x * x;
   }
Pues en Haskell sería así:
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:

1690365549610.png

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:
1690365928894.png

1690365942307.png

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


1690366864114.png


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

1690367116606.png


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

1690367894227.png


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:
  • Regalar
  • Like
Reacciones : destapeman y Haze

LinceCrypt

Tu lince confiable
Noderador
Nodero
Noder
¿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;
   }
Pues en Haskell sería así:
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:

Ver el archivo adjunto 24897
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:
Ver el archivo adjunto 24898
Ver el archivo adjunto 24899
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

Ver el archivo adjunto 24900

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

Ver el archivo adjunto 24901

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 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

Ver el archivo adjunto 24902

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:
Le he dado a algo y se ha subido por la cara el post, JAJAJ le quedan un par de retoques, ya pondré en este comentario en un edit cuando esté listo.

Edit: Ya añadí la última parte, ya está completo todo
 
Última edición:

Haze

Miembro muy activo
Burgués de Nodo
Noderador
Nodero
Noder
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í:
Java:
   int square(int x) {
      return x * x;
   }
Pues en Haskell sería así:
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:

Ver el archivo adjunto 24897
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:
Ver el archivo adjunto 24898
Ver el archivo adjunto 24899
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


Ver el archivo adjunto 24900

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

Ver el archivo adjunto 24901

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

Ver el archivo adjunto 24902

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.
por fin lo hiciste, muy completo y largo lo guardo en marcadores y esta noche me lo miro yo llevo unos años aprendiendo Java
 

LinceCrypt

Tu lince confiable
Noderador
Nodero
Noder
por fin lo hiciste, muy completo y largo lo guardo en marcadores y esta noche me lo miro yo llevo unos años aprendiendo Java
Pues al loro de la parte 3 más o menos de esto, que empezaré a meter estructuras de datos en Java y Haskell.

Estructuras de datos creadas por ti 100% en Java y Haskell
 
  • Like
Reacciones : destapeman