Primeros pasos con Haskell Language

¡Hola Mundo!

Un básico "¡Hola, mundo!" program en Haskell se puede expresar de manera concisa en solo una o dos líneas:

main :: IO ()
main = putStrLn "Hello, World!"

La primera línea es una anotación de tipo opcional, que indica que main es un valor de tipo IO (), que representa una acción de E/S que “calcula” un valor de tipo () (léase “unidad”; el tupla vacía que no transmite información) además de realizar algunos efectos secundarios en el mundo exterior (aquí, imprimiendo una cadena en la terminal). Esta anotación de tipo generalmente se omite para main porque es su único tipo posible.

Ponga esto en un archivo helloworld.hs y compílelo usando un compilador Haskell, como GHC:

ghc helloworld.hs

Ejecutar el archivo compilado dará como resultado que la salida "¡Hola, mundo!" se imprima en la pantalla:

./helloworld
Hello, World!

Alternativamente, runhaskell o runghc permiten ejecutar el programa en modo interpretado sin tener que compilarlo:

runhaskell helloworld.hs

El REPL interactivo también se puede usar en lugar de compilar. Viene con la mayoría de los entornos Haskell, como ghci que viene con el compilador GHC:

ghci> putStrLn "Hello World!"
Hello, World!
ghci> 

Alternativamente, cargue scripts en ghci desde un archivo usando load (o :l):

ghci> :load helloworld

:reload (o :r) recarga todo en ghci:

Prelude> :l helloworld.hs 
[1 of 1] Compiling Main             ( helloworld.hs, interpreted )

<some time later after some edits>

*Main> :r
Ok, modules loaded: Main.

Explicación:

Esta primera línea es una firma de tipo, declarando el tipo de principal:

main :: IO ()

Los valores de tipo IO () describen acciones que pueden interactuar con el mundo exterior.

Debido a que Haskell tiene un [sistema de tipos Hindley-Milner] completo (https://en.wikipedia.org/wiki/Hindley-Milner_type_inference) que permite la inferencia automática de tipos, las firmas de tipos son técnicamente opcionales: si simplemente omite el main :: IO (), el compilador podrá inferir el tipo por sí solo analizando la definición de main. Sin embargo, se considera muy mal estilo no escribir signaturas para definiciones de alto nivel. Las razones incluyen:

  • Las firmas de tipo en Haskell son una pieza de documentación muy útil porque el sistema de tipos es tan expresivo que a menudo se puede ver para qué tipo de cosas es buena una función simplemente mirando su tipo. Se puede acceder cómodamente a esta “documentación” con herramientas como GHCi. Y a diferencia de la documentación normal, el verificador de tipos del compilador se asegurará de que realmente coincida con la definición de la función.

  • Firmas de tipo mantener errores locales. Si comete un error en una definición sin proporcionar su firma de tipo, es posible que el compilador no informe inmediatamente un error, sino que simplemente infiera un tipo sin sentido para él, con el que realmente verifica el tipo. Entonces puede recibir un mensaje de error críptico al usar ese valor. Con una firma, el compilador es muy bueno para detectar errores justo donde ocurren.

Esta segunda línea hace el trabajo real:

main = putStrLn "Hello, World!"

Si proviene de un lenguaje imperativo, puede ser útil tener en cuenta que esta definición también se puede escribir como:

main = do {
   putStrLn "Hello, World!" ;
   return ()
   }

O de manera equivalente (Haskell tiene un análisis basado en el diseño; pero tenga cuidado al mezclar tabulaciones y espacios de manera inconsistente, lo que confundirá este mecanismo):

main = do
    putStrLn "Hello, World!"
    return ()

Cada línea en un bloque do representa algún monádico (aquí, E/S) cálculo, de modo que todo El bloque do representa la acción general compuesta por estos subpasos combinándolos de una manera específica para la mónada dada (para E/S esto significa simplemente ejecutarlos uno tras otro).

La sintaxis do es en sí misma un azúcar sintáctico para las mónadas, como IO aquí, y return es una acción no operativa que produce su argumento sin realizar efectos secundarios o cálculos adicionales que podrían ser parte de una definición de mónada particular.

Lo anterior es lo mismo que definir main = putStrLn "¡Hola, mundo!", porque el valor putStrLn "¡Hola, mundo!" ya tiene el tipo IO (). Visto como una “declaración”, putStrLn "Hello, World!" puede verse como un programa completo, y usted simplemente define main para referirse a este programa.

Puede buscar la firma de putStrLn en línea:

putStrLn :: String -> IO ()
-- thus,
putStrLn (v :: String) :: IO ()

putStrLn es una función que toma una cadena como argumento y genera una acción de E/S (es decir, un valor que representa un programa que el tiempo de ejecución puede ejecutar). El tiempo de ejecución siempre ejecuta la acción denominada main, por lo que simplemente debemos definirla como igual a putStrLn "¡Hola, mundo!".

Factoriales

La función factorial es un Haskell “¡Hola mundo!” (y para la programación funcional en general) en el sentido de que demuestra sucintamente los principios básicos del lenguaje.

Variación 1

fac :: (Integral a) => a -> a
fac n = product [1..n]

Demostración en vivo

  • Integral es la clase de tipos de números enteros. Los ejemplos incluyen Int y Integer.
  • (Integral a) => coloca una restricción en el tipo a para estar en dicha clase
  • fac :: a -> a dice que fac es una función que toma a y devuelve a
  • producto es una función que acumula todos los números en una lista multiplicándolos entre sí.
  • [1..n] es una notación especial que se convierte en enumFromTo 1 n, y es el rango de números 1 ≤ x ≤ n.

Variación 2

fac :: (Integral a) => a -> a
fac 0 = 1
fac n = n * fac (n - 1)

Demostración en vivo

Esta variación utiliza la coincidencia de patrones para dividir la definición de la función en casos separados. La primera definición se invoca si el argumento es 0 (a veces llamada condición de parada) y la segunda definición en caso contrario (el orden de las definiciones es significativo). También ejemplifica la recursividad ya que fac se refiere a sí mismo.


Vale la pena señalar que, debido a las reglas de reescritura, ambas versiones de fac se compilarán en un código de máquina idéntico al usar GHC con las optimizaciones activadas. Entonces, en términos de eficiencia, los dos serían equivalentes.

Empezando

REPL en línea

La forma más fácil de empezar a escribir Haskell es probablemente ir al sitio web de Haskell o Probar Haskell y usar el REPL en línea (read-eval-print-loop) en la página de inicio. El REPL en línea admite la mayoría de las funciones básicas e incluso algunas IO. También hay disponible un tutorial básico que se puede iniciar escribiendo el comando ayuda. Una herramienta ideal para comenzar a aprender los conceptos básicos de Haskell y probar algunas cosas.

GHC(i)

Para los programadores que están listos para involucrarse un poco más, existe GHCi, un entorno interactivo que viene con el Glorious/Glasgow Haskell Compiler. El GHC se puede instalar por separado, pero eso es solo un compilador. Para poder instalar nuevas bibliotecas, también se deben instalar herramientas como [Cabal][cabal] y [Stack][stack]. Si está ejecutando un sistema operativo similar a Unix, la instalación más fácil es instalar [Stack][stack] usando:

curl -sSL https://get.haskellstack.org/ | sh

Esto instala GHC aislado del resto de su sistema, por lo que es fácil de eliminar. Sin embargo, todos los comandos deben estar precedidos por stack. Otro enfoque simple es instalar una Plataforma Haskell. La plataforma existe en dos sabores:

  1. La distribución mínima contiene solo GHC (para compilar) y Cabal/Stack (para instalar y compilar paquetes)
  2. La distribución completa también contiene herramientas para el desarrollo de proyectos, perfiles y análisis de cobertura. También se incluye un conjunto adicional de paquetes ampliamente utilizados.

Estas plataformas se pueden instalar descargando un instalador y siguiendo las instrucciones o usando el administrador de paquetes de su distribución (tenga en cuenta que no se garantiza que esta versión esté actualizada):

-Ubuntu, Debian, Menta:

   sudo apt-get install haskell-platform

-Fedora:

   sudo dnf install haskell-platform
  • Sombrero rojo:

     sudo yum install haskell-platform
    
  • Arco Linux:

     sudo pacman -S ghc cabal-install haskell-haddock-api \
                    haskell-haddock-library happy alex
    
  • Gentoo:

     sudo layman -a haskell
     sudo emerge haskell-platform
    

-OSX con Homebrew:

   brew cask install haskell-platform
  • OSX con puertos Mac:

     sudo port install haskell-platform
    

Una vez instalado, debería ser posible iniciar GHCi invocando el comando ghci en cualquier parte de la terminal. Si la instalación salió bien, la consola debería verse como

[email protected]:~$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Prelude> 

posiblemente con algo más de información sobre qué bibliotecas se han cargado antes del Preludio>. Ahora, la consola se ha convertido en un REPL de Haskell y puede ejecutar el código de Haskell como con el REPL en línea. Para salir de este entorno interactivo, se puede teclear :q o :quit. Para obtener más información sobre qué comandos están disponibles en GHCi, escriba :? como se indica en la pantalla de inicio.

Debido a que escribir las mismas cosas una y otra vez en una sola línea no siempre es tan práctico, podría ser una buena idea escribir el código de Haskell en archivos. Estos archivos normalmente tienen .hs como extensión y se pueden cargar en el REPL usando :l o :load.

Como se mencionó anteriormente, GHCi es parte de GHC, que en realidad es un compilador. Este compilador se puede usar para transformar un archivo .hs con código Haskell en un programa en ejecución. Debido a que un archivo .hs puede contener muchas funciones, se debe definir una función principal en el archivo. Este será el punto de partida del programa. El archivo test.hs se puede compilar con el comando

ghc test.hs

esto creará archivos de objetos y un ejecutable si no hubo errores y la función principal se definió correctamente.

Herramientas más avanzadas

  1. Ya se mencionó anteriormente como administrador de paquetes, pero [stack][stack] puede ser una herramienta útil para el desarrollo de Haskell de maneras completamente diferentes. Una vez instalado, es capaz de

    • installing (multiple versions of) GHC
    • project creation and scaffolding
    • dependency management
    • building and testing projects
    • benchmarking
  2. IHaskell es un kernel haskell para IPython y permite combinar código (ejecutable) con notación matemática y de descuento.

Fibonacci, utilizando la evaluación perezosa

La evaluación perezosa significa que Haskell evaluará solo los elementos de la lista cuyos valores son necesarios.

La definición recursiva básica es:

f (0)  <-  0
f (1)  <-  1
f (n)  <-  f (n-1) + f (n-2)

Si se evalúa directamente, será muy lento. Pero imagina que tenemos una lista que registra todos los resultados,

fibs !! n  <-  f (n) 

Después

                  ┌──────┐   ┌──────┐   ┌──────┐
                  │ f(0) │   │ f(1) │   │ f(2) │
fibs  ->  0 : 1 : │  +   │ : │  +   │ : │  +   │ :  .....
                  │ f(1) │   │ f(2) │   │ f(3) │
                  └──────┘   └──────┘   └──────┘

                  ┌────────────────────────────────────────┐
                  │ f(0)   :   f(1)   :   f(2)   :  .....  │ 
                  └────────────────────────────────────────┘
      ->  0 : 1 :               +
                  ┌────────────────────────────────────────┐
                  │ f(1)   :   f(2)   :   f(3)   :  .....  │
                  └────────────────────────────────────────┘

Esto está codificado como:

fibn n = fibs !! n
    where
    fibs = 0 : 1 : map f [2..]
    f n = fibs !! (n-1) + fibs !! (n-2)

O incluso como

GHCi> let fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

zipWith hace una lista aplicando una función binaria dada a los elementos correspondientes de las dos listas que se le han dado, por lo que zipWith (+) [x1, x2, ...] [y1, y2, ...] es igual a [x1 + y1, x2 + y2, ...].

Otra forma de escribir fibs es con la función scanl:

GHCi> let fibs = 0 : scanl (+) 1 fibs
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

scanl construye la lista de resultados parciales que produciría foldl, trabajando de izquierda a derecha a lo largo de la lista de entrada. Es decir, scanl f z0 [x1, x2, ...] es igual a [z0, z1, z2, ...] donde z1 = f z0 x1; z2 = f z1 x2; ....

Gracias a la evaluación perezosa, ambas funciones definen listas infinitas sin calcularlas por completo. Es decir, podemos escribir una función fib, recuperando el n-ésimo elemento de la sucesión de Fibonacci ilimitada:

GHCi> let fib n = fibs !! n  -- (!!) being the list subscript operator
-- or in point-free style:
GHCi> let fib = (fibs !!)
GHCi> fib 9
34

Primos

Algunas variantes más destacadas:

Por debajo de 100

import Data.List ( (\\) )

ps100 = ((([2..100] \\ [4,6..100]) \\ [6,9..100]) \\ [10,15..100]) \\ [14,21..100]

   -- = (((2:[3,5..100]) \\ [9,15..100]) \\ [25,35..100]) \\ [49,63..100]

   -- = (2:[3,5..100]) \\ ([9,15..100] ++ [25,35..100] ++ [49,63..100])

Ilimitado

Criba de Eratóstenes, utilizando paquete data-ordlist:

import qualified Data.List.Ordered

ps   = 2 : _Y ((3:) . minus [5,7..] . unionAll . map (\p -> [p*p, p*p+2*p..]))

_Y g = g (_Y g)   -- = g (g (_Y g)) = g (g (g (g (...)))) = g . g . g . g . ...

Tradicional

(un tamiz de división de prueba subóptimo)

ps = sieve [2..]
     where
     sieve (x:xs) = [x] ++ sieve [y | y <- xs, rem y x > 0]

-- = map head ( iterate (\(x:xs) -> filter ((> 0).(`rem` x)) xs) [2..] )

División de prueba óptima

ps = 2 : [n | n <- [3..], all ((> 0).rem n) $ takeWhile ((<= n).(^2)) ps]

-- = 2 : [n | n <- [3..], foldr (\p r-> p*p > n || (rem n p > 0 && r)) True ps]

Transición

De la división de prueba al tamiz de Eratóstenes:

[n | n <- [2..], []==[i | i <- [2..n-1], j <- [0,i..n], j==n]]

El código más corto

nubBy (((>1).).gcd) [2..]          -- i.e., nubBy (\a b -> gcd a b > 1) [2..]

nubBy también es de Data.List, como (\\ ).

Declaración de valores

Podemos declarar una serie de expresiones en el REPL así:

Prelude> let x = 5
Prelude> let y = 2 * 5 + x
Prelude> let result = y * 10
Prelude> x
5
Prelude> y
15
Prelude> result
150

Para declarar los mismos valores en un archivo escribimos el siguiendo:

-- demo.hs

module Demo where
-- We declare the name of our module so 
-- it can be imported by name in a project.

x = 5

y = 2 * 5 + x

result = y * 10

Los nombres de los módulos están en mayúscula, a diferencia de los nombres de las variables.