Lazy Evaluation in Haskell
Haskell uses lazy evaluation by default, meaning expressions are not evaluated until their results are actually needed. This enables powerful programming techniques and optimizations.
What is Lazy Evaluation?
In lazy evaluation:
- Expressions are not evaluated when they are bound to variables
- Evaluation is deferred until the value is actually required
- Once evaluated, results are shared (memoized) for subsequent uses
- Only the parts of data structures that are needed are computed
Benefits of Laziness
Lazy evaluation provides several advantages:
-- Infinite lists
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take the first 10 Fibonacci numbers
first10Fibs = take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]
-- Only computes what's needed
sumFirst10 = sum first10Fibs -- Doesn't compute the whole infinite list
Thunks
Unevaluated expressions are stored as "thunks":
-- Thunk example
x = 2 + 3 -- Not evaluated yet
y = x * x -- Another thunk
-- When we actually need the value (e.g., print y)
-- Then x is evaluated to 5, and y to 25
Strict vs Non-strict
Haskell is non-strict, meaning function arguments aren't evaluated before being passed:
-- Non-strict function
const :: a -> b -> a
const x _ = x -- Second argument is never evaluated
-- Example usage
result = const 42 (error "This won't be evaluated")
-- result is 42, no error occurs
Pattern Matching Forces Evaluation
Pattern matching causes evaluation to occur:
-- Forces evaluation of first element
firstElement :: [a] -> a
firstElement (x:_) = x
-- This would force evaluation of the error
-- bad = firstElement [error "Boom!", 2, 3]
Controlling Evaluation
You can control evaluation with strictness annotations:
-- Bang patterns (force evaluation)
{-# LANGUAGE BangPatterns #-}
strictAdd :: Int -> Int -> Int
strictAdd !x !y = x + y -- x and y are evaluated immediately
-- seq function
strictPair :: a -> b -> (a, b)
strictPair x y = x `seq` y `seq` (x, y)
Infinite Data Structures
Laziness enables working with infinite structures:
-- Infinite list of ones
ones :: [Int]
ones = 1 : ones
-- Infinite list of natural numbers
nats :: [Int]
nats = [0..]
-- Sieve of Eratosthenes (infinite primes)
primes :: [Int]
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
Lazy IO
Laziness can be used with IO for efficient processing:
-- Read file lazily
processLargeFile :: FilePath -> IO ()
processLargeFile path = do
contents <- readFile path -- Lazy read
let linesList = lines contents
mapM_ processLine (take 100 linesList) -- Only read first 100 lines
processLine :: String -> IO ()
processLine line = putStrLn ("Processing: " ++ line)
Space Leaks
Laziness can sometimes cause memory issues:
-- Bad: builds up thunks
sumSquares :: [Int] -> Int
sumSquares = foldr (\x acc -> x * x + acc) 0
-- Better: strict accumulator
sumSquares' :: [Int] -> Int
sumSquares' = foldl' (\acc x -> acc + x * x) 0
Strict Data Types
You can define strict fields in data types:
-- Strict data type
data StrictPair a b = StrictPair !a !b
-- Evaluation happens when constructing
pair = StrictPair (1 + 2) (3 + 4) -- Both additions happen immediately
Deepseq for Forcing Evaluation
The deepseq package can force complete evaluation:
import Control.DeepSeq
-- Force complete evaluation
forceEvaluation :: NFData a => a -> a
forceEvaluation x = x `deepseq` x
-- Example usage
main = do
let bigList = [1..1000000] :: [Int]
print (forceEvaluation (sum bigList))
← Previous: Monads & IO |
Next: Concurrent Haskell →