100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Lists and List Comprehensions

Learn how Haskell's linked-list data type works and how list comprehensions let you build, filter, and transform lists with concise, declarative syntax.

Core ConceptsBeginner9 min readJul 10, 2026
Analogies

The List Data Type

In Haskell, a list is a singly linked structure built from two constructors: the empty list [] and the cons operator :, which prepends an element to an existing list — so [1,2,3] is just syntactic sugar for 1 : 2 : 3 : []. Because every element in a list must have the same type ([Int], [Char], [Bool], and so on), the type checker can catch mistakes like mixing numbers and strings in one list at compile time, long before the program runs. Strings in Haskell are literally [Char], which is why string functions like length, reverse, and ++ (concatenation) work identically on a string like "hello" and on [1,2,3] — they're both just lists under the hood.

🏏

Cricket analogy: A Haskell list is like a chain of fielders passing a relay throw from the boundary to the keeper — each fielder (:) holds the ball and points to the next fielder, ending in an empty hand ([]) at the start of the chain.

Building and Combining Lists

Two lists are joined end-to-end with the ++ operator, as in [1,2] ++ [3,4] giving [1,2,3,4], though because ++ must walk the entire left-hand list to reach its end before attaching the right-hand list, repeatedly appending to the right of a long list is inefficient — prepending with : is always O(1) by contrast. Functions like map, filter, and foldr are the workhorses for processing lists: map (*2) [1,2,3] gives [2,4,6] by applying a function to every element, filter even [1,2,3,4] gives [2,4] by keeping elements that satisfy a predicate, and foldr (+) 0 [1,2,3] collapses a list into a single value, here 6, by combining elements with a function from the right.

🏏

Cricket analogy: Joining two lists with ++ is like combining two overs' worth of ball-by-ball commentary into one full innings log — you must read through the first over completely before appending the second over's deliveries.

haskell
-- Building and transforming lists
numbers :: [Int]
numbers = [1, 2, 3, 4, 5]

doubled :: [Int]
doubled = map (*2) numbers          -- [2,4,6,8,10]

evensOnly :: [Int]
evensOnly = filter even numbers     -- [2,4]

total :: Int
total = foldr (+) 0 numbers         -- 15

-- List comprehension: squares of even numbers up to 10
squaresOfEvens :: [Int]
squaresOfEvens = [x * x | x <- [1..10], even x]
-- [4,16,36,64,100]

List Comprehensions

A list comprehension like [x * x | x <- [1..10], even x] reads almost like set-builder notation from mathematics: 'the square of x, for x drawn from 1 to 10, where x is even', producing [4,16,36,64,100]. The part before the pipe (x * x) is the output expression, x <- [1..10] is a generator that binds x to each element in turn, and even x is a guard that filters out elements failing the condition — you can chain multiple generators, like [(x,y) | x <- [1,2], y <- ["a","b"]], to produce every combination, giving nested-loop behavior in a single declarative line.

🏏

Cricket analogy: A list comprehension is like a scorer's shorthand: runs scored by a batsman, for every ball he faced, where the ball resulted in a boundary — one compact rule replaces a page of manual filtering through the ball-by-ball log.

List comprehensions can include multiple generators and guards, and later generators can depend on earlier-bound variables, e.g. [(x,y) | x <- [1..5], y <- [1..x]] produces pairs where y never exceeds x — useful for generating triangular patterns or combinations without repeats.

Because Haskell lists are singly linked and lazily evaluated, using !! (index access) or repeatedly appending with ++ on large lists is O(n) and can silently make code that looks simple run very slowly; if you need fast random access, reach for Data.Vector or Data.Sequence instead of a plain list.

  • Haskell lists are built from [] (empty) and : (cons); [1,2,3] desugars to 1:2:3:[].
  • All elements of a list share one type, and String is literally [Char].
  • ++ concatenates lists but is O(n) in the length of the left list; prepending with : is O(1).
  • map, filter, and foldr are core higher-order functions for transforming and reducing lists.
  • List comprehensions combine an output expression, generators (<-), and guards in one declarative line.
  • Multiple generators in a comprehension behave like nested loops, and later generators can use earlier bindings.
  • For large lists needing fast random access, prefer Data.Vector or Data.Sequence over plain lists.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#HaskellStudyNotes#ListsAndListComprehensions#Lists#List#Comprehensions#Data#DataStructures#StudyNotes#SkillVeris