Esc
Start typing to search...

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 it

Multiple 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 it

Multiline 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 it

The 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 it

This 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 it

Each 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 it

The 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 it

Branch 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 it

Each 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 it

The 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 it

Variables are visible in all nested scopes (blocks indented further), but not in sibling or parent scopes.

Scope Rules

  1. Sequential visibility: Within a block, later statements can see earlier bindings
  2. 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 it

The 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

  1. Use 4 spaces per indentation level (recommended)
  2. Be consistent — don't mix tabs and spaces
  3. Align related code — put sibling statements at the same level
  4. Indent continuations clearly — make it obvious when a line is an argument
  5. Use empty lines to separate logical sections, but maintain indentation

Summary

IndentationMeaning
Further indentedContinues previous expression (as argument)
Same levelSeparate statement in block
Less indentedEnds 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.