One Way to Name a Value
In Zero, you give a value a name with let:
let answer = 42
That's the whole syntax. There's no var, no const, no auto. A single binding keyword keeps the language small — agents and humans both learn it once and apply it everywhere.
let introduces a local binding in the current scope. After this line, answer refers to 42 until the end of the enclosing block.
Type Inference
The compiler infers the type from the right-hand side. The literal 42 has type i32 by default, so answer is an i32. The literal "hello" is a string, so:
let greeting = "hello"
binds greeting to a string value. If you call a function that returns a Pair<i32, u8>, the binding has type Pair<i32, u8>:
let pair = makePair(40, 2_u8)
You don't have to write the type on every binding, which keeps the code legible.
Explicit Type Annotations
When you want to document the type — or force a specific one when inference would pick something different — write the type after a colon:
let count: u8 = 10
let pair: Pair<i32, u8> = makePair(40, 2_u8)
Annotations are also a hint to the compiler when a literal could be one of several types. The literal 10 could be i32, i64, u8, and so on; the annotation pins it down.
You'll also see typed suffixes on literals as an alternative to annotating the binding:
let small = 10_u8 // u8 from the literal suffix
let big = 10_i64 // i64 from the literal suffix
Both forms are valid; pick whichever makes the intent clearer at the call site.
Bindings in Action
A worked example using both inferred and explicit forms — click Run to try it:
point is annotated explicitly because the right-hand side is a struct literal. total is inferred — sum is declared to return i32, so the binding is i32 too.
Scope and Shadowing
A let binding is valid from the line that declares it through the end of its enclosing block. Nested blocks create new scopes:
pub fun main(world: World) -> Void raises {
let value = 1
if true {
let value = 2 // shadows the outer 'value' inside this block
// here, value == 2
}
// back outside the if, value == 1 again
}
The inner value doesn't mutate the outer one — it's a separate binding that goes out of scope at the closing brace of the if block. This is the same model as Rust and ML-family languages. It's especially common when you want to transform a value through a series of steps without inventing new names for each intermediate result.
What let Doesn't Do
A few things you might expect from other languages that let deliberately doesn't include:
- Type-only declarations. There is no
let x: i32;form that introduces an uninitialized binding. A binding must have a value at the point it's declared. - Pattern destructuring (yet). Some languages let you write
let (a, b) = pair. Zero is small by design and currently focuses on plain name bindings — check the current docs for whether destructuring has landed. - Multiple keywords for different lifetimes. No separate
static,const,let mut, or block-scoped vs. function-scoped variants. One keyword.
If your language background is JavaScript, the closest analogy is const — a name bound to a value for the rest of the block, with shadowing in inner scopes. If your background is Rust, let here serves the same role as Rust's let minus the explicit mut keyword.
A Pattern: Building Up a Value Step by Step
Bindings shine when you want to write a computation as a series of named intermediate steps. This is good both for humans reading and for agents reasoning locally about each line:
Each line introduces one new fact for the rest of the function to work with. The compiler still produces tight code — there's no runtime cost to naming the intermediate values.
Next: Primitive Types
let doesn't mean much without something to bind. The next doc walks through Zero's primitive types — the integer widths, floats, strings, and the Void and Bool types you'll see most often.
Frequently Asked Questions
How do you declare a variable in Zero?
Use let. The form is let name = value for an inferred type, or let name: Type = value to write the type explicitly. For example: let answer = 42 or let answer: i32 = 42. Both bind the name answer to the value 42 in the current scope.
Does Zero infer types for let bindings?
Yes. If you write let total = sum(point) and sum returns i32, the binding's type is inferred to be i32. You can still annotate explicitly when you want to document the type or force a specific one — for example let count: u8 = 10.
Are let bindings in Zero mutable?
Plain let introduces a local binding for use within its scope. The mutability story in pre-1.0 Zero is still evolving — the language emphasizes explicit effects and predictable memory, so anything that mutates state through a binding has to make that visible. Check the current Zero docs for the exact mutability syntax in your toolchain version.
What is the difference between let and const in Zero?
Zero uses let for ordinary local bindings inside function bodies. It doesn't expose multiple binding keywords like JavaScript's let/const/var — keeping the surface small is a deliberate design choice. Compile-time constants are typically expressed through the type system or top-level declarations, not a separate keyword.
Can you redeclare a let binding in Zero?
Bindings live within their enclosing scope. A new let with the same name in a nested scope is a separate binding that shadows the outer one for the duration of the inner scope — the outer binding is unaffected once the inner scope ends. This is the same model Rust and ML-family languages use.