Chapter 2 — Values and Bindings
A binding gives a name to a value. Ingle has two keywords for it, and the entire difference between them is whether you’re allowed to change your mind later.
let name = "Ingle" // immutable: this name will always mean "Ingle"
var count = 0 // mutable: this one can change
count = count + 1 // ...like so
let is immutable. Once bound, it stays bound. var is mutable; you can reassign it
as often as you like. Two short words, one each, no modifiers to forget.
If you try to reassign a let, the compiler stops you — and, importantly, it tells you
exactly how to fix it:
let a = 5
a = 6
error: cannot assign to an immutable 'let' binding; declare it with 'var'
This is a theme you’ll see again and again: Ingle’s error messages are written to be read. They name your problem in the words of your program and, where they can, tell you the move that fixes it. The compiler is trying to be a teacher, not a bouncer.
Types are optional here (but allowed)
Locals infer their type from the value, so you rarely annotate them. When you want to be explicit — for clarity, or to pin down a specific numeric width — you can:
let year: int = 2026
let pi: float = 3.14159
A small, tidy detail: a binding’s right-hand side is worked out before the new name exists. So this does what you’d hope:
let a = 10
let a = a + 1 // the 'a' on the right is the old 'a' (10); the new 'a' is 11
The second a shadows the first. The new binding reuses the name, and the old value is
referred to one last time on the way in. This is legal and often handy — refining a value
through a short pipeline of lets without inventing a1, a2, a3.
Constants: a let at the top level
A let written at the top of a file, outside any function, is a module constant — a
named value fixed at compile time:
let WIDTH = 800
let TITLE = "Ingle"
fn main() -> int {
println(TITLE)
return WIDTH
}
Ingle
=> 800
Constants must be initialised with a literal — a number, a string, true/false, or a
negative number. A constant may carry a type annotation, and the compiler honours it: the
literal adopts the declared width — let MAX: u8 = 200 is a u8, not an int — and a value
that doesn’t fit or match the annotation is a compile error (let MAX: u8 = 300 is out of
range; let TITLE: int = "Ingle" is a type mismatch), caught at build, not left to surprise you.
You can’t yet write let x = some_function() at the top level; general
mutable globals aren’t a thing in Ingle today, and honestly they’re a misfeature you’ll not
miss. Constants cost nothing at runtime: each use is substituted with the value directly. They
also make excellent colours, key codes, size limits, and configuration knobs.
The discard: _
Sometimes you want a value’s effect but not the value itself. The underscore _ is Ingle’s
write-only discard: bind to it and the right-hand side is evaluated, then thrown away.
let _ = save(record) // run it for the side effect; ignore the status it returns
for _ in 0..3 { ring() } // do this three times; the loop index isn't needed
The same _ is the catch-all arm in a match (Chapter 8),
and it means the same thing everywhere: I don’t need this one. It is genuinely write-only — you
can’t read _ back, and you can’t name a function, struct, or enum _ either. Try, and the
compiler says so plainly: a function cannot be named '_' — it is the write-only discard, not a
usable name.
Fireside trivia. “Immutable by default” sounds like a modern fad, but it’s closer to a return to manners. The earliest functional languages treated reassignment as the unusual, slightly suspicious operation. Decades of C-family languages then made
x = 5so cheap and so default that we forgot it was a choice. Ingle is quietly siding with the old guard: you can change a value, but you have to look it in the eye and typevarfirst.