cv.cm

Programación · Lenguajes de programación

Tutorial de Haskell para principiantes

8 min de lectura

Respuesta corta

Haskell es un lenguaje puramente funcional, de tipado estático, con inferencia de tipos y evaluación perezosa. Los programas se construyen con funciones puras, tipos de datos algebraicos y coincidencia de patrones, y los efectos secundarios se registran en tipos como IO. Esta página cubre la instalación de GHC, los tipos, las funciones y guardas, las listas y funciones de orden superior, los tipos de datos, las clases de tipos, Maybe y Either, y un ejemplo completo.

Qué es Haskell y para qué se usa

Haskell es un lenguaje de programación puramente funcional con un sistema de tipos estático robusto, inferencia de tipos y evaluación perezosa. Lo diseñó un comité de investigadores y se definió por primera vez en 1990; el estándar actual es Haskell 2010 y GHC (Glasgow Haskell Compiler) es la implementación principal. En Haskell, las funciones no tienen efectos secundarios a menos que su tipo lo indique, lo que hace que los programas sean más fáciles de razonar y de probar.

Haskell se usa en finanzas y fintech, compiladores y herramientas para lenguajes de programación (Pandoc y el linter ShellCheck están escritos en Haskell), proyectos de blockchain, backends web e investigación. Mucha gente también lo aprende porque sus ideas, como los tipos de datos algebraicos, las clases de tipos y las funciones puras, han influido en Rust, Swift, Scala, Kotlin y TypeScript.

Instala GHC y ejecuta Hola, mundo

El instalador recomendado es GHCup, que instala GHC, las herramientas de compilación Cabal y Stack, y el Haskell Language Server para editores. Todo programa tiene una acción main de tipo IO ().

-- hello.hs
main :: IO ()
main = putStrLn "Hello, world!"
runghc hello.hs          # run without compiling
# Hello, world!
ghc hello.hs && ./hello  # compile to a native executable
ghci                     # interactive REPL; load a file with :load hello.hs

Los comentarios empiezan con --. En GHCi, :type expression (o :t) muestra el tipo de cualquier expresión, una de las mejores formas de aprender.

Valores, tipos e inferencia de tipos

Los nombres se enlazan una sola vez y nunca cambian: no hay variables mutables. Las firmas de tipo se escriben con :: y son opcionales, porque el compilador las infiere, pero es una buena práctica escribirlas en las definiciones de nivel superior.

name :: String
name = "Haskell"

year :: Int
year = 1990

ratio :: Double
ratio = fromIntegral year / 1000   -- convert Int to Double explicitly

main :: IO ()
main = do
  putStrLn (name ++ " " ++ show year)  -- Haskell 1990
  print ratio                          -- 1.99
  print (7 `div` 2, 7 `mod` 2)         -- (3,1)
  print (2 ^ 64 :: Integer)            -- 18446744073709551616
TipoEjemploNotas
Int42entero de tamaño fijo (64 bits con GHC en las plataformas habituales)
Integer2 ^ 100entero de precisión arbitraria
Double3.14coma flotante de 64 bits
BoolTrue, Falseconstructores con inicial mayúscula
Char'h'un carácter Unicode
String"text"una lista de Char, [Char]; Data.Text es más rápido

show convierte un valor en un String, y print es putStrLn . show. Los números nunca se convierten de forma implícita; usa fromIntegral, round o truncate.

Funciones, guardas y coincidencia de patrones

La aplicación de funciones se escribe con espacios, no con paréntesis: add 2 3. Toda función recibe un argumento y devuelve otra función para el resto, una característica llamada currificación, así que add 2 es en sí misma una función. La coincidencia de patrones y las guardas sustituyen a la mayoría de las cadenas de if.

add :: Int -> Int -> Int
add x y = x + y

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

grade :: Int -> Char
grade score
  | score >= 90 = 'A'
  | score >= 70 = 'B'
  | otherwise   = 'C'

describe :: [a] -> String
describe []  = "empty"
describe [_] = "one item"
describe _   = "many items"

main :: IO ()
main = do
  print (add 2 3)                 -- 5
  print (map (add 10) [1, 2, 3])  -- [11,12,13] (partial application)
  print (factorial 20)            -- 2432902008176640000
  print (grade 75)                -- 'B'
  putStrLn (describe "hi")        -- many items
  print (if even 4 then "even" else "odd")  -- "even"

if es una expresión y siempre necesita un else. No hay bucles: la repetición se hace con recursión o con funciones de orden superior como map, filter y foldr.

Listas, tuplas y funciones de orden superior

Las listas contienen valores de un solo tipo y son la colección más común. Las tuplas agrupan un número fijo de valores que pueden ser de tipos distintos. Las listas por comprensión, los rangos y el operador de composición de funciones . hacen que el procesamiento de datos sea conciso.

import Data.List (sortOn)

main :: IO ()
main = do
  let nums = [5, 3, 8, 1] :: [Int]
  print (head nums, length nums, sum nums)       -- (5,4,17)
  print (0 : nums)                               -- [0,5,3,8,1] (prepend)
  print (filter even nums)                       -- [8]
  print (foldr (+) 0 [1 .. 10])                  -- 55
  print [x * x | x <- [1 .. 5], odd x]           -- [1,9,25]
  print (take 5 [1, 3 ..])                       -- [1,3,5,7,9] (lazy infinite list)
  let people = [("Ann", 31), ("Bo", 25)] :: [(String, Int)]
  print (map fst (sortOn snd people))            -- ["Bo","Ann"]
  print ((length . filter (> 2)) nums)           -- 3
  print (zip [1 :: Int ..] "abc")                -- [(1,'a'),(2,'b'),(3,'c')]

Como Haskell es perezoso, los valores se calculan solo cuando se necesitan. Por eso take 5 [1, 3 ..] funciona con una lista infinita. La pereza también puede acumular grandes expresiones sin evaluar; prefiere foldl' de Data.List en lugar de foldl para sumar listas largas.

Tipos de datos algebraicos y clases de tipos

Defines tus propios tipos con data. Un tipo puede tener varios constructores (un tipo suma), cada uno con campos (un tipo producto). Las clases de tipos como Show, Eq, Ord y Functor describen comportamientos compartidos, de forma parecida a las interfaces; deriving genera automáticamente las instancias más comunes.

data Shape = Circle Double | Rect Double Double
  deriving (Show, Eq)

area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * h

data User = User { userName :: String, userAge :: Int }
  deriving Show

class Describable a where
  describe :: a -> String

instance Describable Shape where
  describe (Circle _) = "a circle"
  describe (Rect _ _) = "a rectangle"

main :: IO ()
main = do
  print (map area [Circle 1, Rect 2 3])   -- [3.141592653589793,6.0]
  putStrLn (describe (Rect 1 1))          -- a rectangle
  let u = User { userName = "Ann", userAge = 31 }
  print (u { userAge = 32 })              -- User {userName = "Ann", userAge = 32}

Maybe, Either e IO

Haskell no tiene null ni excepciones no comprobadas en el código puro. Un valor que puede faltar tiene el tipo Maybe a (Just x o Nothing), y un cálculo que puede fallar con un motivo tiene el tipo Either e a (Left err o Right x). Los efectos secundarios, como leer la entrada, viven en el tipo IO, y la notación do los encadena en secuencia.

import Text.Read (readMaybe)

safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "division by zero"
safeDiv x y = Right (x `div` y)

parseAge :: String -> Maybe Int
parseAge s = case readMaybe s of
  Just n | n >= 0 -> Just n
  _               -> Nothing

main :: IO ()
main = do
  print (safeDiv 10 2)          -- Right 5
  print (safeDiv 1 0)           -- Left "division by zero"
  print (parseAge "42")         -- Just 42
  print (parseAge "abc")        -- Nothing
  print (fmap (+ 1) (Just 41))  -- Just 42
  putStrLn "Your name?"
  n <- getLine                  -- an IO action that returns a String
  putStrLn ("Hi, " ++ n)

Un programa pequeño y completo: frecuencia de palabras

Este programa lee texto de la entrada estándar, cuenta la frecuencia de las palabras con un Map e imprime las tres más comunes. Usa composición de funciones, Data.Map del paquete containers que se incluye con GHC, y ordenación.

import Data.Char (isAlpha, toLower)
import Data.List (sortBy)
import Data.Ord (comparing, Down (..))
import qualified Data.Map.Strict as Map

wordFreq :: String -> [(String, Int)]
wordFreq =
  sortBy (comparing (Down . snd) <> comparing fst)
    . Map.toList
    . Map.fromListWith (+)
    . map (\w -> (w, 1))
    . words
    . map (\c -> if isAlpha c then toLower c else ' ')

main :: IO ()
main = do
  text <- getContents
  mapM_ (\(w, n) -> putStrLn (w ++ ": " ++ show n)) (take 3 (wordFreq text))
echo "The cat and the dog. The dog sat, and the cat ran." | runghc wordfreq.hs
# the: 4
# and: 2
# cat: 2

Siguientes pasos

  • La página de documentación de haskell.org, que enlaza a tutoriales, libros y la guía del usuario de GHC.
  • Hackage, el repositorio de paquetes, y Hoogle, un buscador que encuentra funciones por nombre o por tipo.
  • Learn You a Haskell for Great Good! y Real World Haskell, dos libros que se pueden leer gratis.
  • El Haskell 2010 Language Report, para la definición precisa del lenguaje.

Preguntas frecuentes

¿Haskell es difícil de aprender?

Se siente distinto porque no tiene variables mutables ni bucles y usa un sistema de tipos expresivo. Quienes empiezan y aprenden la recursión, la coincidencia de patrones y los tipos paso a paso suelen encontrar que el lenguaje básico es pequeño y coherente.

¿Qué significa puramente funcional?

Que las funciones siempre devuelven el mismo resultado para la misma entrada y no cambian el estado ni realizan entrada y salida. Los efectos siguen siendo posibles, pero se expresan en tipos como IO.

¿Qué es una mónada en Haskell?

Una mónada es una clase de tipos para tipos como Maybe, Either e IO que permiten encadenar cálculos con >>=. La notación do es azúcar sintáctico para ese encadenamiento.

¿Qué es la evaluación perezosa?

Las expresiones se evalúan solo cuando se necesita su valor. Esto permite listas infinitas y evita trabajo innecesario, pero puede hacer que el uso de memoria sea más difícil de predecir.

¿Debo usar Stack o Cabal?

Ambos compilan proyectos de Haskell y ambos se instalan con GHCup. Cabal es la herramienta de compilación estándar; Stack añade instantáneas de paquetes seleccionadas. Cualquiera de los dos sirve para principiantes.

Todos los tutoriales de programación