Skip to content

0004: Email Tutorials Haskell For Beginners ‐ Where Clauses and Let Expression Basics in Haskell

Bernard Sibanda edited this page Dec 16, 2025 · 3 revisions

LigerLearn Video 4

📑 Table of Contents

  • 4.1. Describing a List with a Function
  • 4.2. Using length and show to Build the Description
  • 4.3. Introducing where Clauses to Organize Logic
  • 4.4. Visibility and Privacy of where Bindings
  • 4.5. Indentation Rules and Whitespace for where
  • 4.6. Introducing let Expressions
  • 4.7. Structure, Scope, and Indentation in let Expressions
  • 4.8. Comparing where Clauses and let Expressions as a Beginner

4.1. Describing a List with a Function

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.

4.2. Using length and show to Build the Description

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.

4.3. Introducing where Clauses to Organize Logic

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.

4.4. Visibility and Privacy of where Bindings

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.

4.5. Indentation Rules and Whitespace for where

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.

4.6. Introducing let Expressions

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.

4.7. Structure, Scope, and Indentation in let Expressions

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.

4.8. Comparing where Clauses and let Expressions as a Beginner

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.

Glossary of Terms (LigerLearn Video 4)

  • 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 printable String.
  • Type conversion (via show) — Turning a value like an Int into a String representation.
  • Prelude — The default standard library automatically available in most Haskell files/contexts.
  • String concatenation — Joining text pieces together into one String (commonly using ++).
  • where clause — 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/let are 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.
  • let expression — An expression that introduces local bindings and then uses them via in.
  • in keyword — Separates let bindings from the expression that uses them.
  • Declaration (inside let) — A name/value definition written under let.
  • Expression — Code that evaluates to a value (e.g., the part after in).
  • Syntactic construct — A language form with special syntax rules (e.g., where attached 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 where for tidy function bodies, use let when you need an expression form).

Quizz & Progress Badge NFT

Clone this wiki locally