-
Notifications
You must be signed in to change notification settings - Fork 4
0004: Email Tutorials Haskell For Beginners ‐ Where Clauses and Let Expression Basics in Haskell
- 4.1. Describing a List with a Function
- 4.2. Using
lengthandshowto Build the Description - 4.3. Introducing
whereClauses to Organize Logic - 4.4. Visibility and Privacy of
whereBindings - 4.5. Indentation Rules and Whitespace for
where - 4.6. Introducing
letExpressions - 4.7. Structure, Scope, and Indentation in
letExpressions - 4.8. Comparing
whereClauses andletExpressions as a Beginner
In this lesson, we start with a simple but very illustrative example: a function that takes a list of Int values and returns a String describing how many items are in that list. The idea is to accept a list as input and then produce a human-readable sentence as output, something like “The list has 3 items.” This example is helpful because it touches on several important ideas at once: working with lists, using library functions such as length, converting values from one type to another using show, and finally, structuring your code using where clauses and let expressions to keep it clear and readable.
The first implementation of the function, which we can call description1, directly combines all the pieces needed to produce the final string. It takes a list of integers as input, uses the length function (from the Prelude) to count how many elements are in the list, and then converts that count from an Int into a String using the show function. Because Haskell does not allow direct concatenation of a String with an Int, the conversion step is essential. The show function solves this by turning the numeric value into its string representation. Finally, the function uses the string concatenation operator to join the text fragments together into a single result. When you call the function with a list, you get back a complete descriptive sentence as expected.
Although the first version of the function works, all of the logic is written inline in a single expression. This can quickly become hard to read with more complex functions. To address this, Haskell provides where clauses, which allow you to break the logic of a function into smaller, named pieces. In the alternative implementation, again taking a list of Int values and returning a String, a where block is introduced at the end of the function definition. Inside this block, intermediate results are given names, such as a value representing the length of the list and another representing its string form. The main function body then becomes simpler and more readable, as it only needs to refer to these named values instead of nesting all function calls directly.
The bindings that you create inside a where clause are private to the function they belong to. This means that other functions in the same source file cannot see or use them. In the example, names such as lengthI and lengthS exist only inside the scope of the function where they are defined. They help clarify what each step is doing—lengthI might hold the integer result of length x, and lengthS might hold the result of applying show to that integer. While type signatures for these local bindings are optional, including them can make the code more explicit and easier to understand. This pattern encourages you to structure your functions in a way that separates concerns and keeps related logic grouped together without polluting the wider namespace.
In Haskell, whitespace and indentation are part of the syntax, and where clauses are a good example of this. The where keyword must appear on a new line, and it must be indented slightly more than the function name it belongs to. The common convention is to indent the where keyword by two spaces relative to the start of the function definition. The body of the where clause—the lines that define local bindings—must then be indented even further, typically by four spaces from the original function start. All lines inside the where body need to be aligned to the same column. If these indentation rules are not followed, the compiler will produce a parse error, often indicating that the whitespace needs to be fixed. While this may feel strict at first, the rules are in fact intuitive and help keep the structure of the code visually clear.
After understanding where clauses, the lesson introduces another feature: let expressions. A let expression serves a similar purpose to a where clause in that it allows you to introduce local bindings, but it does so in a slightly different form. A let expression consists of two main parts: a list of declarations following the let keyword, and an expression following the in keyword. The declarations define local names and values, and the expression after in is where those declarations are in scope and can be used. To connect this to the earlier example, the same descriptive function can be written using a let expression that breaks down the steps of calling length and show before constructing the final string.
When using a let expression, you begin with the let keyword and then list your declarations, one per line, each describing a local binding you want to create. After these declarations, you write the in keyword followed by the final expression that uses those bindings. The overall let expression is then assigned to a function or value name, such as description2. Conceptually, description2 is equal to whatever value the expression after in evaluates to. As with where clauses, you can place variable bindings, function bindings, and optional type signatures inside the let section. Indentation remains important: technically, the code will compile as long as each line of the let expression is indented at least one space beyond the function declaration above. However, the recommended style is to align the entire let expression neatly in one column, making the code easier to read at a glance.
Although where clauses and let expressions have different syntax, they serve broadly similar purposes: both introduce a private scope where you can define local bindings and use them to simplify your main expression. One visible difference is the order in which things appear. In a let expression, the bindings appear first (after let), and the final expression appears after in. In a where clause, the main expression appears first, and the supporting bindings come later in the where block. Another important distinction is that a where block is a syntactic construct attached to a declaration, whereas a let expression is itself an expression and can therefore be used anywhere an expression is allowed. As a beginner, a practical guideline is to use where clauses by default to tidy up complex function bodies, and to reach for let expressions in situations where a where clause cannot be attached naturally. Over time, as you grow more comfortable with Haskell, you can adjust your personal style, but this simple rule of thumb is a solid starting point.
-
List — An ordered collection of values in Haskell (e.g., a list of
Ints). -
Int— A standard integer type. -
String— Text data (in Haskell, a list of characters). - Function — A named piece of code that takes input(s) and produces an output.
- Input (argument) — The value you pass into a function.
- Output (result) — The value a function returns.
-
length— A function that returns how many elements are in a list. -
show— A function that converts a value into a printableString. -
Type conversion (via
show) — Turning a value like anIntinto aStringrepresentation. - Prelude — The default standard library automatically available in most Haskell files/contexts.
-
String concatenation — Joining text pieces together into one
String(commonly using++). -
whereclause — A block attached to a definition that introduces local helper bindings. - Binding — Associating a name with a value (or expression).
- Local binding — A binding that exists only inside a limited region (like inside a function).
- Intermediate result — A step value stored in a local name to simplify the main expression.
- Scope — The region where a name can be used.
- Visibility — Whether a name can be accessed from a given place in the code.
-
Private (to a function) — Names defined in
where/letare not usable outside that function/expression. - Type signature — A declaration of a value/function’s type (optional for many local bindings).
- Whitespace / Indentation — Spaces and alignment that affect how Haskell parses code structure.
- Indentation rule — The requirement that related lines align correctly so the compiler understands blocks.
- Parse error — A syntax error, often caused by incorrect indentation/structure.
-
letexpression — An expression that introduces local bindings and then uses them viain. -
inkeyword — Separatesletbindings from the expression that uses them. -
Declaration (inside
let) — A name/value definition written underlet. -
Expression — Code that evaluates to a value (e.g., the part after
in). -
Syntactic construct — A language form with special syntax rules (e.g.,
whereattached to a declaration). - Namespace — The set of names available in a given context; local bindings avoid “polluting” it.
-
Rule of thumb — A practical beginner guideline (e.g., prefer
wherefor tidy function bodies, useletwhen you need an expression form).
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: