Declaring a Shape
A shape is a record type with named, typed fields:
shape Point {
x: i32,
y: i32,
}
Pieces:
shapeintroduces the declaration.Pointis the type's name.- The braces enclose a list of fields, each
name: Type.
That declaration adds a new type called Point to the current scope. Anywhere you can use a built-in type like i32, you can now also use Point.
Constructing a Shape Value
Create an instance with a struct-literal expression:
let point = Point { x: 40, y: 2 }
The literal names the shape and assigns each field. Every field must be present — Zero doesn't silently default missing fields to zero or null. If you forget one, the compiler tells you so:
{
"code": "FLD002",
"message": "missing field: y",
"line": 4
}
(The exact error code may vary; the principle of "no implicit defaults" is the constant.)
You can read or annotate the type explicitly if you want it to be visible:
let point: Point = Point { x: 40, y: 2 }
Reading Fields
Field access uses dot syntax:
let point = Point { x: 40, y: 2 }
let xVal = point.x
let yVal = point.y
point.x reads the x field. There's no get_x() method — fields are plain data.
A Full Worked Example
This is the canonical point.0 example from the language's repository — click Run to try it:
Walk through it from the top:
- Declare a
Pointshape with twoi32fields. - Define a
sumfunction that takes aPointand returns the sum of its fields. - In
main, construct aPoint, callsum, and compare the result.
Three steps, three shapes-related ideas (declare, construct, access), and one effect — the check world.out.write(...) at the end. Notice that sum doesn't touch World. It's a pure function over data, and the signature makes that obvious.
Shapes with Nested Fields
A shape can hold values of any type, including other shapes:
shape Range {
start: i32,
end: i32,
}
shape Segment {
label: String,
range: Range,
}
let seg = Segment {
label: "warmup",
range: Range { start: 0, end: 10 },
}
Field access chains as you'd expect:
let len = seg.range.end - seg.range.start
Generic Shapes
When a shape's fields should be type-polymorphic, declare type parameters in angle brackets:
shape Pair<T, U> {
left: T,
right: U,
}
Instances pin the parameters to concrete types:
let intBytePair: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hello", right: "world" }
A type alias can shorten a common parameterization:
type BytePair = Pair<u8, u8>
let bytes: BytePair = Pair { left: 1_u8, right: 2_u8 }
Generics covers type parameters in more depth — including on functions, not just shapes.
What Shapes Are Not
A few things you might expect from a "struct" in another language that shapes deliberately don't include:
- No methods. A shape declaration is just data. Behavior lives in free functions that take the shape as a parameter. This mirrors the same separation between data and effects you see in functions.
- No inheritance. Shapes don't extend other shapes. If you want shared structure, factor it into a common field or build a sum type with choice.
- No implicit constructors or destructors. Construction is the struct-literal expression. Cleanup is explicit — when the standard library exposes resources that need disposal, it's done through capability-style APIs rather than hidden RAII.
- No private fields. A shape's fields are all accessible to code that can see the shape's type. Visibility is at the type level, not the field level.
The pattern is: shapes are simple, predictable record types, and you build everything else from them.
When to Use a Shape vs. a Choice
Quick guide:
- Use a shape when a value has all of these fields together. A
Pointalways has both anxand ay. - Use a choice when a value is one of several alternatives. A
Resultis either anokor anerr. - Use an enum when the alternatives carry no extra data — they're just labels. Days of the week, simple states.
These three building blocks — shape (and), choice (or), enum (or with no payload) — cover almost every data-modeling need.
Next: Generics
You've seen Pair<T, U> show up in passing. The next doc, generics, explains how type parameters work on both shapes and functions, including the patterns that appear throughout the Zero standard library.
Frequently Asked Questions
What is a shape in Zero?
A shape is Zero's struct-like product type — a named record with typed fields. You declare it with shape Name { field1: T1, field2: T2 }, construct values with Name { field1: v1, field2: v2 }, and read fields with dot syntax (value.field1). Shapes are the building block for modeling structured data.
How do you create a shape value?
Use a struct-literal expression that names the shape and assigns each field: let point = Point { x: 40, y: 2 }. Every field has to be filled in — Zero doesn't quietly default missing fields. The order of fields in the literal doesn't have to match the declaration.
How is shape different from a class?
A shape is plain data — it has fields, but no methods, no inheritance, no implicit constructors. Functions that operate on a shape take it as a parameter explicitly. That separation keeps the language small and keeps the cost of building or copying a shape predictable, with no hidden vtables or destructors.
Can shapes be generic in Zero?
Yes. Declare type parameters in angle brackets: shape Pair<T, U> { left: T, right: U }. Instances pin those parameters down: Pair<i32, u8>. Generic shapes show up throughout the standard library — Maybe<T>, Span<T>, and so on are all generic shapes or sum types built on the same idea.
Are shapes copied or referenced when passed to functions?
Pass-by-value is the default mental model for shapes — the callee sees its own logical copy of the data, not a reference into the caller's binding. The exact memory model in pre-1.0 Zero is still evolving (you'll see ref and mutref in the standard library examples for explicit reference types). For most application code, treat shape parameters as value-typed inputs.