Menu

Zero Generics: Type Parameters on Functions and Shapes

How generics work in Zero: declaring type parameters on functions and shapes, calling generic functions, and the type-alias pattern that turns long parameterizations into clean names.

This page includes runnable editors — edit, run, and see output instantly.

Why Generics

You quickly run into types that should work for more than one element type. A "pair" of two values doesn't care whether the values are integers, strings, or some user-defined shape. Generics let you write the type once and instantiate it with whatever element types the caller needs.

The alternative — writing IntPair, StringPair, BytePair, and so on — gets boring fast and doesn't compose. Generics in Zero are the standard tool for the job.

Generic Functions

Declare type parameters in angle brackets between the function name and the parameter list:

fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
    return Pair { left: left, right: right }
}

T and U are placeholder types — the caller decides what they are.

Calls usually don't need to spell them out; the compiler infers from the argument types:

let pair = makePair(40, 2_u8)

Here T is inferred to be i32 (the default for an unsuffixed integer literal) and U is u8 (from the _u8 suffix). The resulting binding pair has type Pair<i32, u8>.

If inference would pick the wrong types — for example, because the literals are ambiguous — you can pin the parameters at the call site:

let pair = makePair<u8, u8>(1, 2)

(Whether the angle-bracket call syntax is exactly as shown may vary across Zero versions; check the current docs for the precise spelling. The inference-first behavior is the part that's stable.)

Generic Shapes

Shapes take type parameters in the same way:

shape Pair<T, U> {
    left: T,
    right: U,
}

Each field's type can mention the parameters. Instances pin them down:

let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words:    Pair<String, String> = Pair { left: "hi", right: "there" }

A worked example combining a generic shape and a generic function — click Run to see inference at work:

One generic shape declaration, one generic function, one strongly-typed call site. No IntBytePair boilerplate.

Type Aliases

When the same parameterized type comes up over and over, give it a name with type:

type BytePair = Pair<u8, u8>

Now BytePair is interchangeable with Pair<u8, u8> anywhere you can write a type:

An alias is just a naming feature — it doesn't create a distinct type. A function that takes a BytePair will happily accept a value of type Pair<u8, u8> (and vice versa).

Generics in the Standard Library

The same mechanism powers a lot of the standard library. A few you'll see in real Zero code:

  • Maybe<T> — an optional value, holding either a T or nothing.
  • Span<T> — a borrowed slice over T values. Span<u8> is the canonical view over a byte buffer.
  • ref<T> and mutref<T> — explicit reference types for cases where you need to share data without copying.

You don't have to learn these all at once. The point of generics is that the same shape works for whatever element type you have on hand.

When Generics Pay Off (and When They Don't)

Reach for generics when you find yourself writing the same function or shape twice with different element types. Reach for a concrete type when:

  • The function's logic only makes sense for a specific type (a parser for Strings, say).
  • You want the type to appear in error messages so debugging is easier.
  • The performance characteristics depend on a specific size in memory.

The cost of generics is real — bigger binaries (each instantiation generates new code) and slightly slower compile times. For most application code that cost is negligible, but it's worth knowing about when you're building tight, embedded-style code where binary size matters.

A Note on Constraints

Some generic systems let you constrain a type parameter ("T must support ==", "T must implement Iterator"). Zero's constraint story in pre-1.0 is still evolving — the examples in the official repo use generics in their plain form, without elaborate bounds. As the language settles, expect constraint syntax to land in a small, regular form consistent with the rest of the language. For now, write generics that work for any T you actually pass them, and let the compiler tell you when an operation isn't supported.

Next: Enums

Generics let you parameterize over types. The next building block is at the other end of the spectrum — enums, Zero's plain enumeration type for cases where the variants carry no extra data.

Frequently Asked Questions

How do generics work in Zero?

Declare type parameters in angle brackets after the function or shape name: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> or shape Pair<T, U> { left: T, right: U }. Callers either pin the parameters explicitly (Pair<i32, u8>) or let the compiler infer them from the call's arguments.

Can shapes be generic in Zero?

Yes. A shape can take type parameters in the same angle-bracket syntax used on functions: shape Pair<T, U> { left: T, right: U }. Each field can use the parameters in its type. Instances are formed by writing the parameterized type — Pair<i32, u8> for example.

Do you have to specify type parameters when calling a generic function?

Usually no. The compiler infers them from the argument types. Calling makePair(40, 2_u8) is enough — T becomes i32 and U becomes u8. You can pin the parameters explicitly when inference would pick the wrong type or when you want the call site to document them.

What is a type alias in Zero?

A type alias is a shorthand name for a longer type expression. type BytePair = Pair<u8, u8> lets you write BytePair anywhere you'd otherwise write Pair<u8, u8>. The alias is a pure naming feature — it doesn't introduce a new type, just a shorter way to refer to an existing one.

Where do generics show up in Zero's standard library?

All over — wherever a type needs to hold or operate on an arbitrary element type. Maybe<T> for optional values, Span<u8> for byte slices, container types parameterized over their element. The same generic mechanism handles user-defined and standard-library types.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED