Haskell Language Tutorial
Empezando con Haskell Language
Buscar..
Observaciones
Haskell es un lenguaje de programación avanzado puramente funcional.
caracteristicas:
- Escrito estáticamente: cada expresión en Haskell tiene un tipo que se determina en el momento de la compilación. La verificación de tipo estático es el proceso de verificación de la seguridad de tipo de un programa basado en el análisis del texto de un programa (código fuente). Si un programa pasa un verificador de tipo estático, entonces se garantiza que el programa satisfaga algún conjunto de propiedades de seguridad de tipo para todas las entradas posibles.
- Puramente funcional : cada función en Haskell es una función en el sentido matemático. No hay declaraciones ni instrucciones, solo expresiones que no pueden mutar variables (locales o globales) ni acceder a estados como el tiempo o números aleatorios.
- Concurrente: su compilador insignia, GHC, viene con un recolector de basura paralelo de alto rendimiento y una biblioteca de concurrencia liviana que contiene una serie de primitivas y abstracciones de concurrencia útiles.
- Evaluación perezosa: las funciones no evalúan sus argumentos. Retrasa la evaluación de una expresión hasta que se necesita su valor.
- Uso general: Haskell está diseñado para ser utilizado en todos los contextos y entornos.
- Paquetes: la contribución de código abierto a Haskell es muy activa con una amplia gama de paquetes disponibles en los servidores de paquetes públicos.
El último estándar de Haskell es Haskell 2010. A partir de mayo de 2016, un grupo está trabajando en la próxima versión, Haskell 2020.
La documentación oficial de Haskell es también un recurso completo y útil. Excelente lugar para encontrar libros, cursos, tutoriales, manuales, guías, etc.
Versiones
Versión | Fecha de lanzamiento |
---|---|
Haskell 2010 | 2012-07-10 |
Haskell 98 | 2002-12-01 |
¡Hola Mundo!
Un básico "¡Hola mundo!" El programa 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 ()
(lea "unidad"; la tupla vacía no transmite información) además de realizar algunos efectos secundarios en el mundo exterior (aquí, imprimir una cadena en el terminal). Esta anotación de tipo se suele omitir para main
porque es su único tipo posible.
Coloca esto en un archivo helloworld.hs
y compílalo usando un compilador de Haskell, como GHC:
ghc helloworld.hs
La ejecución del archivo compilado dará como resultado la salida "Hello, World!"
siendo impreso a la pantalla:
./helloworld
Hello, World!
Alternativamente, runhaskell
o runghc
hacen posible ejecutar el programa en modo interpretado sin tener que compilarlo:
runhaskell helloworld.hs
El REPL interactivo también se puede utilizar en lugar de compilar. Se entrega con la mayoría de los entornos de Haskell, como ghci
que viene con el compilador de 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
) :reload
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 main
:
main :: IO ()
Los valores de tipo IO ()
describen acciones que pueden interactuar con el mundo exterior.
Debido a que Haskell tiene un sistema de tipo Hindley-Milner completo que permite la inferencia de tipos automática, las firmas de tipos son técnicamente opcionales: si simplemente omite main :: IO ()
, el compilador podrá inferir el tipo por sí mismo. Analizando la definición de main
. Sin embargo, se considera un estilo muy malo no escribir firmas de tipo para definiciones de nivel superior. Las razones incluyen:
Las firmas de tipos en Haskell son una pieza de documentación muy útil porque el sistema de tipos es tan expresivo que a menudo se puede ver qué tipo de cosa es buena para una función simplemente observando 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!
Las firmas de tipos mantienen los errores locales . Si comete un error en una definición sin proporcionar su tipo de firma, es posible que el compilador no informe un error de inmediato, sino que simplemente infiera un tipo sin sentido para el mismo, con el que realmente verifica. Luego puede obtener un mensaje de error críptico cuando use 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 las pestañas y los 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 cálculo monádico (aquí, E / S), de modo que todo el bloque do
representa la acción general que comprende estos subpasos combinándolos de una manera específica a la mónada dada (para I / O esto significa simplemente ejecutándolos uno tras otro).
La sintaxis do
es en sí misma un azúcar sintáctico para las mónadas, como IO
aquí, y el return
es una acción no operativa que produce su argumento sin realizar efectos secundarios ni cálculos adicionales que puedan formar parte de una definición de mónada particular.
Lo anterior es lo mismo que definir main = putStrLn "Hello, World!"
, porque el valor putStrLn "Hello, World!"
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.
Puedes consultar la firma de putStrLn
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 "Hello, World!"
.
Factorial
La función factorial es un Haskell "Hello World!" (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]
-
Integral
es la clase de tipos de números integrales. Los ejemplos incluyenInt
yInteger
. -
(Integral a) =>
coloca una restricción en el tipoa
para estar en dicha clase -
fac :: a -> a
dice quefac
es una función que toma unaa
y devuelve unaa
-
product
es una función que acumula todos los números en una lista al multiplicarlos. -
[1..n]
es una notación especial que se aplica aenumFromTo 1 n
, y es el rango de números1 ≤ x ≤ n
.
Variación 2
fac :: (Integral a) => a -> a
fac 0 = 1
fac n = n * fac (n - 1)
Esta variación utiliza la coincidencia de patrones para dividir la definición de función en casos separados. La primera definición se invoca si el argumento es 0
(a veces se denomina condición de detención) y la segunda definición de lo contrario (el orden de las definiciones es significativo) También ejemplifica la recursión como fac
refiere a sí mismo.
Vale la pena señalar que, debido a las reglas de reescritura, ambas versiones de fac
se compilarán con 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.
Fibonacci, utilizando la evaluación perezosa
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)
Entonces
┌──────┐ ┌──────┐ ┌──────┐
│ 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 se codifica 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
una lista aplicando una función binaria dada a los elementos correspondientes de las dos listas que se le asignaron, 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 foldl
produciría, 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, ...] where 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 elemento nth de la secuencia 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
Empezando
REPL en linea
La forma más fácil de comenzar a escribir Haskell es probablemente ir al sitio web de Haskell o Probar Haskell y usar el REPL (read-eval-print-loop) en línea 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 un tutorial básico disponible que puede iniciarse escribiendo la help
comando. 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 compilador Haskell de Glorious / Glasgow . 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 y Stack . Si está ejecutando un sistema operativo similar a Unix, la instalación más sencilla es instalar Stack utilizando:
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 ir precedidos de la stack
. Otro enfoque simple es instalar una plataforma Haskell . La plataforma existe en dos sabores:
- La distribución mínima contiene solo GHC (para compilar) y Cabal / Stack (para instalar y construir paquetes)
- La distribución completa además 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, Mint:
sudo apt-get install haskell-platform
Fedora:
sudo dnf install haskell-platform
Sombrero rojo:
sudo yum install haskell-platform
Arch 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 MacPorts:
sudo port install haskell-platform
Una vez instalado, debería ser posible iniciar GHCi invocando el comando ghci
en cualquier parte del terminal. Si la instalación salió bien, la consola debería verse como
me@notebook:~$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/ :? for help
Prelude>
posiblemente con más información sobre qué bibliotecas se han cargado antes del Prelude>
. Ahora, la consola se ha convertido en una REPL de Haskell y puede ejecutar el código de Haskell como en la REPL en línea. Para salir de este entorno interactivo, se puede escribir :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
para una extensión y se pueden cargar en el REPL usando :l
o :load
.
Como se mencionó anteriormente, GHCi es una parte del 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 main
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 main
se definió correctamente.
Herramientas mas avanzadas
Ya se mencionó anteriormente como administrador de paquetes, pero la pila puede ser una herramienta útil para el desarrollo de Haskell de maneras completamente diferentes. Una vez instalado, es capaz de
- Instalación (múltiples versiones de) GHC
- Creación de proyectos y andamios.
- gestión de dependencias
- proyectos de construcción y pruebas
- evaluación comparativa
IHaskell es un núcleo de haskell para IPython y permite combinar código (ejecutable) con markdown y notación matemática.
Primes
Algunas variantes más sobresalientes :
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
Tamiz de Eratóstenes, usando el paquete de datos-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]
Transicional
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 (\\)
.
Declarar 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 lo siguiente:
-- 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úsculas, a diferencia de los nombres de variables.