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.
-- 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 to1:2:3:[]. - All elements of a list share one type, and
Stringis literally[Char]. ++concatenates lists but is O(n) in the length of the left list; prepending with:is O(1).map,filter, andfoldrare 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.VectororData.Sequenceover plain lists.
Practice what you learned
1. What are the two constructors that build every Haskell list?
2. Why is `String` compatible with functions like `reverse` and `length`?
3. What does `filter even [1,2,3,4,5,6]` evaluate to?
4. In the comprehension `[x*x | x <- [1..10], even x]`, what role does `even x` play?
5. Why is repeatedly using `++` to append to the end of a long list considered inefficient?
Was this page helpful?
You May Also Like
Pattern Matching in Haskell
Learn how Haskell lets you destructure values directly in function definitions and case expressions to write clear, exhaustive branching logic.
Recursion in Haskell
Understand why recursion is the primary looping mechanism in Haskell, how base cases and recursive cases work, and how tail recursion and laziness affect performance.
Pure Functions and Immutability
Understand what makes a Haskell function pure, why referential transparency matters, and how immutability changes the way you update data and reason about programs.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics