Indentation and Scope
Keel uses significant indentation to define code structure, similar to Python or Haskell. Understanding how indentation works is essential for writing correct Keel code.
The Core Rule
Within a block, lines at the same indentation level are separate statements. Indentation is also used for function bodies, case branches, and other constructs that require nested content.
For function arguments, the rule is more specific: collections (lists, records, tuples, parenthesized expressions) on an indented new line continue as arguments to the previous function call.
Basic Blocks
Function bodies must be indented from the function definition:
-- Function body must be indented
fn addOne : Int -> Int
fn addOne x =
x + 1
addOne 5 -- 6
Try itMultiple statements at the same indentation form a block:
-- Multiple let bindings at same indentation form a block
fn compute : Int -> Int
fn compute x =
let a = x + 1
let b = a * 2
let c = b - 3
c
compute 5 -- 9
Try itMultiline Function Arguments
Function arguments can be placed on new indented lines, but only for collections (lists, records, tuples) and parenthesized expressions.
Collections as Arguments
Lists, records, and tuples on an indented new line continue as arguments:
-- Multiline arguments work with COLLECTIONS (lists, records, tuples)
-- NOT with simple values like integers
import List
-- This works: list on new indented line continues as argument
let result = List.map (|x| x * 2)
[1, 2, 3]
result -- [2, 4, 6]
Try itThe list [1, 2, 3] is indented from List.map (|x| x * 2), so it's parsed as the next argument.
Elm-style Multiline Lists
For readability, you can use leading commas with lists on multiple lines:
-- Multi-line function calls work with collections on new lines
import List
-- Lambda on same line, list on indented new line
let doubled = List.map (|x| x * 2)
[ 1
, 2
, 3
]
-- Equivalent to: List.map (|x| x * 2) [1, 2, 3]
doubled -- [2, 4, 6]
Try itThis style (common in Elm) keeps each element aligned and makes diffs cleaner.
Separate Statements
Lines at the same indentation level are separate expressions, evaluated in sequence:
-- Lines at the SAME indentation are separate expressions
let x = 1
let y = 2
let z = 3
-- Each let is a separate statement, evaluated in sequence
-- The final expression is the result
x + y + z -- 6
Try itEach let binding starts at the same column, making them separate statements in the block.
Multiline Arguments: Module Functions
Multiline argument continuation works with module functions (like List.map, String.length). You can place collections or parenthesized expressions on indented new lines:
import List
-- These both work with module functions:
List.take
(2)
[1, 2, 3, 4, 5] -- Result: [1, 2]
List.map (|x| x * 2)
[1, 2, 3] -- Result: [2, 4, 6]
Local Functions: Same-line Arguments
For local functions defined with fn or let, arguments should stay on the same line:
fn add : Int -> Int -> Int
fn add x y = x + y
-- Correct: arguments on same line
let result = add 1 2
-- NOT correct: indented line becomes separate expression
let result = add 1
2 -- This is a separate expression, not an argument!
Pipes and Method Chains
Pipe operators work naturally with multiline formatting. Each pipe continues the expression:
-- Pipes work naturally with multiline formatting
import List
let result = [1, 2, 3, 4, 5]
|> List.map (|x| x * 2)
|> List.filter (|x| x > 4)
|> List.foldl (|acc x| acc + x) 0
result -- 6 + 8 + 10 = 24
Try itThe pipes are indented from the initial list, so they continue as part of the same expression chain.
Case Expressions
Case branches must be indented from the case keyword:
-- Case branches must be indented from 'case'
let value = 2
let result = case value of
1 -> "one"
2 -> "two"
_ -> "other"
result -- "two"
Try itBranch bodies can span multiple lines with further indentation:
-- Case branch bodies can span multiple lines
let value = 2
let result = case value of
1 ->
let x = 10
x + 1
2 ->
let x = 20
let y = 5
x + y
_ -> 0
result -- 25
Try itEach branch body is indented from the ->, and statements within the body are at the same level.
If-Then-Else
Multi-line if expressions work similarly:
-- If-then-else branches with multiline bodies
let condition = True
let result =
if condition then
let a = 1
let b = 2
a + b
else
let a = 10
a * 2
result -- 3
Try itThe then and else branches are indented, and multiple statements within each branch are at the same indentation level.
Scoping
Indentation creates nested scopes. Inner scopes can access variables from outer scopes:
-- Indentation creates nested scopes within a block
fn example : Int -> Int
fn example x =
let a = x + 1
let b = a * 2 -- 'a' is visible here
let c = a + b -- both 'a' and 'b' are visible
c
example 5 -- a=6, b=12, c=18 → 18
Try itVariables are visible in all nested scopes (blocks indented further), but not in sibling or parent scopes.
Scope Rules
- Sequential visibility: Within a block, later statements can see earlier bindings
- Not inner to outer: Variables defined in inner scopes are not visible outside
fn example : Int -> Int
fn example n =
let a = n + 1 -- 'n' visible from parameter
let b = a * 2 -- 'a' visible from earlier binding
let c = a + b -- both 'a' and 'b' visible
c
-- 'a', 'b', and 'c' are not visible here
Lists and Records
Collection literals can span multiple lines using Elm-style leading comma syntax:
-- Lists can span multiple lines (Elm-style)
import List
let numbers =
[ 1
, 2
, 3
, 4
, 5
]
List.sum numbers -- 15
Try it-- Records can span multiple lines (Elm-style)
let person =
{ name = "Alice"
, age = 30
, city = "Paris"
}
person.name -- "Alice"
Try itThe leading comma style aligns elements neatly and makes git diffs cleaner (adding an element only changes one line).
Common Mistakes
Mistake 1: Accidental Continuation
-- WRONG: 'print' is parsed as argument to 'result'
let result = compute x
print result
-- CORRECT: 'print' at same level as 'let'
let result = compute x
print result
Mistake 2: Missing Indentation
-- WRONG: body not indented
fn example : Int -> Int
fn example x =
x + 1 -- Error: expected indented block
-- CORRECT: body indented
fn example : Int -> Int
fn example x =
x + 1
Mistake 3: Inconsistent Indentation
-- WRONG: mixed indentation in block
fn example : Int -> Int
fn example x =
let a = 1
let b = 2 -- Error: inconsistent indentation
a + b
-- CORRECT: consistent indentation
fn example : Int -> Int
fn example x =
let a = 1
let b = 2
a + b
Best Practices
- Use 4 spaces per indentation level (recommended)
- Be consistent — don't mix tabs and spaces
- Align related code — put sibling statements at the same level
- Indent continuations clearly — make it obvious when a line is an argument
- Use empty lines to separate logical sections, but maintain indentation
Summary
| Indentation | Meaning |
|---|---|
| Further indented | Continues previous expression (as argument) |
| Same level | Separate statement in block |
| Less indented | Ends current block, returns to outer scope |
Understanding this rule unlocks the rest of Keel's syntax. When in doubt, remember: indented = continuation, same level = separate.
Next Steps
Continue to learn about functions in detail.