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 = 5 so 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 type var first.