Recursion
Haskell does not have for, while, do … while, or any other loop construct. The mechanism for iteration is recursion — direct recursion when the structure of the input dictates the recursion shape, or higher-order functions (map, foldr, replicate, iterate, mapM_) when the iteration follows a conventional pattern. The combination admits expressing every loop in idiomatic Haskell, often more concisely than the C-family forms; the discipline is to recognise which form is appropriate for each situation.
This page covers direct recursion, the principal recursion patterns, the higher-order functions that subsume most loops, and the conventions for using each.
Direct recursion
A recursive function calls itself, typically on a smaller version of its input:
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]
The pattern: a base case (no recursion) and one or more recursive cases (call on a smaller input). The recursion terminates when the base case is reached.
Direct recursion is the principal iteration mechanism in Haskell. The compiler admits it natively, the language has no syntactic overhead for it, and most non-trivial functions are recursive in some form.
Pattern matching and recursion
Recursive functions conventionally pattern-match on their argument:
sum :: Num a => [a] -> a
sum [] = 0
sum (x:xs) = x + sum xs
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
| p x = x : filter p xs
| otherwise = filter p xs
The pattern of “case-split on the structure, recurse on the smaller piece” is the conventional Haskell idiom for list processing.
Tail recursion
A tail-recursive function’s recursive call is the last operation in the function:
-- Not tail-recursive (the `n *` happens after the recursive call):
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- Tail-recursive:
factorial :: Int -> Int
factorial = go 1
where
go !acc 0 = acc
go !acc n = go (acc * n) (n - 1)
The tail-recursive form uses an accumulator — a variable that carries the in-progress result through the recursion. The !acc (with the BangPatterns extension) ensures the accumulator is forced each step, preventing thunk accumulation.
In strict languages, tail recursion matters because tail-call optimisation eliminates stack growth. In Haskell, the situation is more subtle:
- Lazy evaluation means the recursive call may not actually consume stack until the result is forced.
- Tail-recursive functions still allocate thunks for the accumulator unless the accumulator is forced (with
!orseq). - For numeric folds,
foldl'is the conventional strict variant offoldl.
The principal practical guidance:
- For numeric accumulators, use
foldl'fromData.Listor write a tail-recursive helper with a strict accumulator. - For list-producing functions, write the non-tail-recursive form — Haskell’s lazy evaluation produces the result incrementally:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
The non-tail-recursive form is the right choice here because the result is consumed lazily; the apparent “stack growth” never materialises.
Mutual recursion
Two or more functions may call each other:
isEven :: Int -> Bool
isEven 0 = True
isEven n = isOdd (n - 1)
isOdd :: Int -> Bool
isOdd 0 = False
isOdd n = isEven (n - 1)
Mutual recursion is occasionally useful for state-machine-style code or for tree traversals where each level alternates with the next. The compiler handles it natively.
The principal higher-order functions
A substantial fraction of explicit recursion in Haskell code can be replaced by higher-order functions:
map
Apply a function to every element of a list:
map :: (a -> b) -> [a] -> [b]
map (* 2) [1, 2, 3, 4] -- [2, 4, 6, 8]
map show [1, 2, 3] -- ["1", "2", "3"]
map (+ 1) [1..10] -- [2, 3, 4, ..., 11]
map is the conventional iteration when the body produces one result per input.
filter
Keep elements satisfying a predicate:
filter :: (a -> Bool) -> [a] -> [a]
filter even [1..10] -- [2, 4, 6, 8, 10]
filter (> 5) [1..10] -- [6, 7, 8, 9, 10]
filter (not . null) ["", "a", "", "b"] -- ["a", "b"]
foldr and foldl
Reduce a list by applying a binary function:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldl :: (b -> a -> b) -> b -> [a] -> b
foldr (+) 0 [1, 2, 3, 4] -- 10
foldr (\x acc -> x : acc) [] [1, 2, 3] -- [1, 2, 3]; foldr is "right fold"
foldl (+) 0 [1, 2, 3, 4] -- 10 (lazy; may produce thunks)
foldl' (+) 0 [1, 2, 3, 4] -- 10 (strict; preferred)
The principal trade-offs:
foldrbuilds the result from the right; works on infinite lists if the function is lazy in its second argument.foldlbuilds from the left; does not work on infinite lists and may produce thunks.foldl'is the strict left fold; the conventional choice for numeric accumulators.
The conventional advice:
- Use
foldrwhen the function naturally produces the result from the right (list-building, lazy operations). - Use
foldl'for numeric or other strict accumulations.
zip, zipWith
Pair elements of two lists:
zip :: [a] -> [b] -> [(a, b)]
zip [1, 2, 3] ['a', 'b', 'c'] -- [(1,'a'), (2,'b'), (3,'c')]
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith (+) [1, 2, 3] [10, 20, 30] -- [11, 22, 33]
zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
zipWith3 :: (a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d]
zip and zipWith admit iterating over two (or three) parallel lists.
take, drop
Take or drop the first n elements:
take :: Int -> [a] -> [a]
take 3 [1..10] -- [1, 2, 3]
drop :: Int -> [a] -> [a]
drop 3 [1..10] -- [4, 5, 6, 7, 8, 9, 10]
Both are lazy; take 3 from an infinite list takes the first three.
iterate and replicate
Generate sequences:
iterate :: (a -> a) -> a -> [a]
iterate (* 2) 1 -- [1, 2, 4, 8, 16, ...]
iterate (+ 1) 0 -- [0, 1, 2, 3, ...]
replicate :: Int -> a -> [a]
replicate 5 'x' -- "xxxxx"
replicate 3 [1, 2] -- [[1,2], [1,2], [1,2]]
iterate produces an infinite list; replicate produces a finite list.
repeat and cycle
repeat :: a -> [a]
repeat 'x' -- "xxxxxxxxxxx..."
cycle :: [a] -> [a]
cycle [1, 2, 3] -- [1, 2, 3, 1, 2, 3, ...]
Both produce infinite lists; conventional in combination with take.
mapM_ and forM_ (for IO)
Iterate effectfully over a list:
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()
forM_ :: (Foldable t, Monad m) => t a -> (a -> m b) -> m ()
main = mapM_ print [1, 2, 3, 4, 5]
-- prints each on a new line
main = forM_ [1..10] $ \n -> do
putStrLn $ "n = " ++ show n
The mapM_ discards the results; mapM collects them:
results <- mapM compute [1..100] -- collect IO results
forM_ items $ \item -> -- iterate effectfully, discard
process item
The treatment is in IO.
List comprehensions
A list comprehension is a compact list-building syntax:
squares :: [Int]
squares = [x * x | x <- [1..10]]
-- [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
evens :: [Int]
evens = [x | x <- [1..20], even x]
-- [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
pairs :: [(Int, Int)]
pairs = [(x, y) | x <- [1..3], y <- [1..3], x /= y]
-- [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]
primes :: [Int]
primes = [n | n <- [2..], not (any (\p -> n `mod` p == 0) [2..n-1])]
-- the prime sieve, in one line
The form: [<expression> | <generator>, <generator>, ..., <guard>]. Each generator (x <- xs) iterates over a source; each guard filters; the expression is computed for each combination.
List comprehensions are syntactic sugar for concatMap and filter:
[x * x | x <- xs] = map (\x -> x * x) xs
[x | x <- xs, p x] = filter p xs
[(x, y) | x <- xs, y <- ys] = concatMap (\x -> map (\y -> (x, y)) ys) xs
The conventional contemporary advice is to use list comprehensions for simple cases (filter, map, generate) and to fall back to explicit map/filter for the more complex cases.
The Foldable and Traversable classes
Many higher-order operations work on any Foldable or Traversable type, not just lists:
import Data.Foldable
sum :: (Foldable t, Num a) => t a -> a
length :: Foldable t => t a -> Int
elem :: (Foldable t, Eq a) => a -> t a -> Bool
import Data.Traversable
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)
The classes admit applying the conventional list operations to Maybe, Either, Map, Set, Tree, and any user-defined type that admits the right structure. The treatment is in Functors and applicatives and Data structures.
Common iteration patterns
Count something
count :: (a -> Bool) -> [a] -> Int
count p = length . filter p
countMatches = count (> 100) values
Or:
import Data.List (foldl')
count :: (a -> Bool) -> [a] -> Int
count p = foldl' (\acc x -> if p x then acc + 1 else acc) 0
The two forms are equivalent for finite lists; the foldl' version may be marginally faster (one pass instead of two).
Iterating with side effects
import Control.Monad (forM_)
main = forM_ urls $ \url -> do
response <- fetch url
saveResponse response
The pattern admits effectful iteration in IO or any other monad.
Iterating with index
import Data.List (zip)
main = mapM_ (\(i, x) -> putStrLn (show i ++ ": " ++ show x))
(zip [0..] items)
Or:
import Data.List (zip)
processWithIndex :: [a] -> [b]
processWithIndex xs = [process i x | (i, x) <- zip [0..] xs]
The conventional Haskell idiom for “iterate with the position”.
Iterating until condition
takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile (< 100) [1..] -- [1, 2, 3, ..., 99]
dropWhile :: (a -> Bool) -> [a] -> [a]
dropWhile (< 5) [1..10] -- [5, 6, 7, 8, 9, 10]
iterate :: (a -> a) -> a -> [a]
takeWhile (< 1000) (iterate (* 2) 1) -- [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
The combination of iterate and takeWhile admits “iterate until the condition” without an explicit loop.
Until convergence
import Data.List (find)
iterate :: (a -> a) -> a -> [a]
converge :: Eq a => (a -> a) -> a -> a
converge f x = case find (uncurry (==)) (zip iter (tail iter)) of
Just (a, _) -> a
where
iter = iterate f x
The pattern admits “iterate until two consecutive values are equal” — the conventional fixed-point construction in functional programming.
A note on “where are my loops”
Programmers coming from C-family languages frequently ask “where are my loops?” The answer:
- Iteration over a known source:
map,filter,mapM_, list comprehension. - Reduction:
foldr,foldl'. - Iteration with state: tail-recursive helper with accumulator, or the
Statemonad. - Iteration until condition:
iterate+takeWhile/find. - Effectful iteration:
forM_,mapM_,traverse_. - Truly explicit loop: hand-written recursion.
The combination covers every case the C-family loop forms cover, often more concisely. The discipline is to recognise the pattern and use the appropriate higher-order function; the explicit recursion form is a fallback for cases not covered by the conventional toolkit.
The lack of imperative loops is a substantial part of Haskell’s identity. The conventional advice for new Haskell code: try first with the higher-order functions; fall back to explicit recursion only when the higher-order form would be unclear.