Control Flow in Haskell
Learn control flow in Haskell — if-expressions, guards, case expressions, pattern matching, recursion, and list comprehensions with Docker-ready examples
Control flow is how a program decides what to do next. In most languages this means statements that mutate state — if blocks that run for their side effects, for and while loops that count and accumulate into variables. Haskell is purely functional, so it approaches the same problems from a completely different angle.
In Haskell there are no statements, only expressions that evaluate to values. An if in Haskell is not a branch you “execute” — it is an expression that produces a result, which is why it always needs both a then and an else. There are no traditional loops either: because variables are immutable, you cannot increment a counter. Instead, recursion replaces loops, pattern matching and guards replace most conditionals, and list comprehensions and higher-order functions express iteration declaratively.
This tutorial covers the tools Haskell gives you for choosing between values and repeating work: if/then/else expressions, guards, case expressions, pattern matching, recursion as iteration, and list comprehensions. Because Haskell is statically typed with inference, the compiler checks that every branch of a decision produces the same type — a class of bugs simply cannot occur.
If-Then-Else as an Expression
In Haskell, if is an expression that returns a value, so it must always have an else. There is no “if without else” — every branch must produce a result of the same type.
Create a file named conditionals.hs:
| |
Because if is an expression, you can use it anywhere a value is expected — inside a let, as a function argument, or as the body of a function. There is no need for a separate ternary operator; if/then/else already fills that role.
Guards: Choosing Between Conditions
When a function needs to pick a result based on several boolean conditions, guards are cleaner than nested if expressions. Each guard is a condition introduced by |; the first one that is True wins. The catch-all otherwise is simply defined as True.
Create a file named guards.hs:
| |
Guards are evaluated top to bottom, so order matters — put the most specific conditions first. Note how describe uses pattern matching (matching against the literal values 0, 1, 2) with _ as a wildcard for everything else. This is often the most idiomatic replacement for a switch statement.
Case Expressions and Pattern Matching
The case expression matches a single value against a series of patterns. It is Haskell’s equivalent of switch, but far more powerful: patterns can destructure data, and you can attach guards to each branch.
Create a file named case_match.hs:
| |
Pattern matching is the heart of control flow in Haskell. The pattern (w:_) matches a non-empty list, binding w to the first element and ignoring the rest. The empty-list pattern [] handles the other case. Because the compiler can warn about missing patterns, you are nudged toward handling every possibility — a real safety benefit over forgetting a default case elsewhere.
Recursion and List Comprehensions Instead of Loops
Haskell has no for or while loops, because there are no mutable counters to update. Repetition is expressed with recursion, and iteration over collections is expressed with list comprehensions or higher-order functions like map and mapM_.
Create a file named iteration.hs:
| |
Each recursive function has a base case (countdown 0, sumTo 0) that stops the recursion and a recursive case that reduces the problem toward that base. The list comprehension [x | x <- xs, even x] reads as “collect each x drawn from xs where x is even” — the filtering condition replaces an if inside a loop body. For side-effecting iteration, mapM_ runs an IO action once per element, much like a for-each loop.
Running with Docker
Run each example using the official Haskell image — no local installation required.
| |
Expected Output
Running conditionals.hs:
x is odd
bigger value: 7
sign of -3: negative
sign of 0: zero
Running guards.hs:
negative
zero
small
large
none
one
a couple
many
ABCF
Running case_match.hs:
weekend
weekday
hello
(empty)
warm
Running iteration.hs:
[5,4,3,2,1,0]
5050
[2,4,6,8,10]
line 1
line 2
line 3
Key Concepts
ifis an expression, not a statement — it always returns a value, so it must include boththenandelse, and both branches must have the same type.- Guards (
|) select a result based on boolean conditions, evaluated top to bottom;otherwiseis the catch-all and is justTrue. - Pattern matching on literals,
[], and(x:xs)is the idiomatic replacement forswitchand is checked by the compiler for completeness. caseexpressions match a value against patterns and can attach guards to each branch for range checks.- Recursion replaces loops — every recursive function needs a base case to terminate and a recursive case that moves toward it.
- List comprehensions like
[x | x <- xs, even x]express iteration and filtering declaratively, without mutable counters. - Higher-order functions (
map,filter,mapM_) iterate over collections;mapM_is the functional equivalent of afor-each loop for IO actions. - The type system enforces consistency — every branch of a decision must produce the same type, catching a whole class of mistakes at compile time.
Comments
Loading comments...
Leave a Comment