Variables
Variables in Keel are bindings that associate names with values. By default, all bindings are immutable.
Declaring Variables
Use let to declare a variable:
let name = "Alice"
let age = 30
let pi = 3.14159
pi
Try itType Annotations
Keel has strong type inference, but you can add explicit type annotations:
let count : Int = 42
let message : String = "Hello"
let ratio : Float = 0.75
ratio
Try itType annotations are optional but can improve code clarity and catch errors earlier.
Variable Shadowing
To "update" a variable, use variable shadowing by re-declaring it with the same name:
let x = 10
let x = x + 5
x -- 15
Try itVariables itself are immutable. Shadowing creates a new variable that hides the previous one.
let x = 1
let x = x + 1
let x = "hello" -- x can change type when shadowed
x -- "hello"
Try itNote: Only variables support shadowing. Functions, modules, and type definitions do not allow shadowing — redeclaring them with the same name is an error.
Scope
Variables are block-scoped. A variable is only accessible within the block where it's defined:
fn example : Int -> Int
fn example x =
let inner = x + 1
inner
inner -- Error: `inner` is not in scope
Try itLet Expressions
In Keel, let is an expression that introduces bindings:
let area =
let width = 10
let height = 5
width * height
area -- 50
Try itThe value of a let block is the value of its final expression.
Multiple Bindings
You can chain multiple let bindings:
let x = 1
let y = 2
let z = 3
x + y + z
Try itDestructuring
Variables can be bound using pattern matching (destructuring):
Tuple Destructuring
let (x, y) = (10, 20)
x -- 10
Try itRecord Destructuring
let { name, age } = { name = "Bob", age = 25 }
name -- "Bob"
Try itNested Destructuring
let (x, { a, b }) = (1, { a = 2, b = 3 })
a -- 2
Try itRecursive Let Bindings
A lambda bound with let can call itself by name — the binding is in scope for its own body:
-- Recursive lambdas bound with let
-- expect: 120
let fac = |n: Int| if n == 0 then 1 else n * fac (n - 1)
fac 5
Try itThis is useful for local helper functions that don't need to be visible outside a block.
Polymorphic Let Bindings
When a let-bound lambda has no type annotation, it is automatically generalized to work with any type. This is called let-polymorphism:
-- A let-bound lambda can be used at multiple types (let-polymorphism)
-- expect: 1
let identity = |x| x
let n = identity 1
let s = identity "hello"
n
Try itThe lambda |x| x is inferred as ∀a. a -> a, so it can be applied to both Int and String in the same program.
Best Practices
- Use descriptive names:
userNameis better thanu - Use shadowing for transforms: Chain
let x = ...to transform values step by step - Keep scopes small: Define variables close to where they're used
- Use type annotations for public APIs and complex expressions
- Use destructuring to extract values from tuples and records
Next Steps
Learn about operators to combine values.