coldwa.st
Todas las guíasProgramaciónWebDatosHerramientasBases de datosHaskellConceptosCabal y buildsToolchainCompiladorRendimientoEditor y HLS

Haskell · conceptos · evaluación

La evaluación perezosa en Haskell, explicada

Por ColdwastActualizado el 14 de junio de 20268 min de lectura#haskell#laziness#concepts
Código fuente en una pantalla
Evaluación perezosa: Haskell calcula un valor solo en el momento en que algo realmente lo necesita.

La mayoría de los lenguajes son estrictos: una expresión se evalúa en cuanto se vincula a un nombre. Haskell es diferente — es perezoso (más precisamente, no estricto) por defecto: una expresión no se evalúa hasta que su resultado se necesita de verdad. Esta única decisión de diseño está detrás de algunos de los poderes más distintivos de Haskell y de su trampa más conocida. Esta guía explica qué es la evaluación perezosa, cómo funcionan los thunks, qué te aporta y cómo desactivarla cuando lo necesitas.

La idea en una frase

Bajo la evaluación perezosa, vincular un valor no lo calcula — Haskell almacena un cómputo diferido (un thunk) y solo lo fuerza cuando otro cómputo exige el resultado. Si el resultado nunca se exige, el trabajo nunca se hace.

Thunks: cómputo diferido

Cuando escribes let x = expensive 42, Haskell no ejecuta expensive. Crea un thunk — una promesa sin evaluar de expensive 42. El thunk se fuerza solo cuando algo necesita el valor real de x (por ejemplo, imprimirlo o hacer pattern matching sobre él). Fuérzalo una vez y el resultado se cachea para que no se vuelva a calcular.

let x = expensive 42   -- nothing runs yet; x is a thunk
let y = x + 1          -- still nothing; y is a thunk too
print y                -- NOW x and y are forced and evaluated

Qué te aporta la pereza

Líneas de código en una pantalla oscura
Líneas de código en una pantalla — la pereza te permite definir estructuras infinitas y evaluar solo la parte que consumes.
  • Estructuras de datos infinitas. [1..] es la lista infinita de enteros. take 5 [1..] devuelve [1,2,3,4,5] porque solo se exigen los cinco primeros.
  • Composición sin desperdicio. Puedes encadenar filter y map sobre una lista enorme y Haskell fusiona el trabajo, produciendo solo lo que el consumidor extrae — sin listas intermedias completas si nunca se exigen del todo.
  • Cortocircuito gratis. False && undefined devuelve False sin tocar nunca undefined, porque el segundo argumento nunca se fuerza.
  • Autorreferencia. Clásicos de una línea como fibs = 0 : 1 : zipWith (+) fibs (tail fibs) definen el flujo infinito de Fibonacci — solo posible porque la estructura se construye de forma perezosa.

La trampa: las fugas de espacio

La pereza tiene un coste. Los thunks sin evaluar se acumulan en memoria, y una larga cadena de cómputos diferidos puede disparar silenciosamente el uso del heap — una fuga de espacio. El caso de manual es un fold por la izquierda perezoso que construye una torre gigantesca de thunks (((0 + 1) + 2) + 3) ... en lugar de ir sumando sobre la marcha:

foldl (+) 0 [1..1000000]    -- builds a million thunks, may blow the stack
foldl' (+) 0 [1..1000000]   -- strict fold: adds as it goes, constant space

La solución es forzar la evaluación donde importa. Recurre al fold por la izquierda estricto foldl' (de Data.List) para las acumulaciones.

Forzar la evaluación estricta: seq, $! y BangPatterns

Haskell te da herramientas precisas para exigir la evaluación:

  • seq a b — fuerza a a forma normal de cabeza débil antes de devolver b.
  • ($!) — aplicación estricta: f $! x fuerza x antes de aplicar f.
  • BangPatterns — la extensión {-# LANGUAGE BangPatterns #-} te permite escribir let !x = ... para forzar una vinculación.

Una sutileza que conviene saber: forzar normalmente llega a la forma normal de cabeza débil (el constructor más externo), no a la evaluación completa. Para forzar en profundidad, usa force / deepseq del paquete deepseq.

Preguntas frecuentes

¿Es Haskell perezoso o no estricto? Técnicamente no estricto (una garantía semántica); GHC lo implementa mediante evaluación perezosa con thunks y compartición. En la práctica se dice "perezoso".

¿Por qué mi programa Haskell usa tanta memoria? A menudo es una fuga de espacio por thunks acumulados — comúnmente un foldl perezoso. Cambia a foldl' o añade rigor con seq/BangPatterns.

¿Qué es un thunk? Un cómputo diferido y sin evaluar almacenado en lugar de un valor, forzado solo cuando se exige el valor y luego cacheado.

¿La pereza hace que Haskell sea lento? No de forma inherente — puede evitar trabajo innecesario y permitir la fusión. Pero la pereza descuidada provoca fugas de espacio; saber cuándo forzar es parte de escribir buen Haskell.

La pereza es la misma maquinaria que hace que las demás abstracciones de Haskell se compongan limpiamente — consulta cómo funciona la secuenciación en nuestra guía sobre las mónadas en Haskell, y configura el toolchain para probar estos ejemplos en GHCi con la guía del compilador GHC.

Guía independiente y mantenida por la comunidad. coldwa.st es un sitio de recursos de programación; este artículo es texto explicativo nuevo y original sobre la evaluación perezosa en Haskell, no afiliado a ningún proyecto ni escrito por él. El código refleja el comportamiento estándar de Haskell/GHC — verifícalo con la documentación actual de GHC.