13: Generics and Type Constraints

Audience: Experienced programmers Time: 120 minutes Prerequisites: 02-Values-and-Types, 08-Interfaces-and-Protocols You'll learn: Generic types, type parameters, constraints, practical uses, common pitfalls


The Big Picture

So far, you've written code that works with specific types: a List(int), a HashMap(str, int). But what if you want a function that works with any type? That's where generics come in.

Generics let you write code that's type-safe but reusable across different types:

class Container(T)

var item as T

def get as T return item

Container(int) holds integers. Container(str) holds strings. Same code, different types. This is safer than using object or any, because the type checker still verifies you don't mix types.


Generic Classes

!Generics Instantiation

The simplest generic is a container that holds a single value:

// file: 13_generic_container.zbr

// teaches: generic class definition // chapter: 13-Generics-and-Type-Constraints

class Container(T) var item as T

def set(value as T) item = value

def get as T return item

class Main shared def main var int_box = Container(int)() int_box.set(42) print int_box.get() # Output: 42

var str_box = Container(str)() str_box.set("hello") print str_box.get() # Output: hello

Notice the syntax: - Container(T)T is a type parameter - Container(int)() — substitute int for T, then create an instance - Inside the class, T can be used like any other type

You can have multiple type parameters:

// file: 13_generic_pair.zbr

// teaches: multiple type parameters // chapter: 13-Generics-and-Type-Constraints

class Pair(K, V) var key as K var value as V

def set_key(k as K) key = k

def set_value(v as V) value = v

def get_key as K return key

def get_value as V return value

class Main shared def main var p = Pair(str, int)() p.set_key("count") p.set_value(42) print "Key: ${p.get_key()}, Value: ${p.get_value()}"


Generic Methods

You can also write generic methods within regular classes:

// file: 13_generic_methods.zbr

// teaches: generic methods // chapter: 13-Generics-and-Type-Constraints

class Utils shared def identity(value as T) as T return value

def first_of_three(a as T, b as T, c as T) as T return a

class Main shared def main var x = Utils.identity(42) print x # Output: 42

var y = Utils.identity("hello") print y # Output: hello

var z = Utils.first_of_three(1, 2, 3) print z # Output: 1

The type parameter T is inferred from the arguments you pass.


Generic Collections

You already use generics implicitly with List and HashMap:

// file: 13_generic_collections.zbr

// teaches: using generic stdlib types // chapter: 13-Generics-and-Type-Constraints

class Main shared def main var numbers as List(int) = List() numbers.add(1) numbers.add(2) numbers.add(3)

for n in numbers print n

var ages as HashMap(str, int) = HashMap() ages.put("Alice", 30) ages.put("Bob", 25)

for name, age in ages print "${name}: ${age}"

These are all generic types. The standard library provides them pre-built.


Type Constraints

Sometimes you want a generic that works with any type that implements an interface:

// file: 13_type_constraints.zbr

// teaches: interface constraints // chapter: 13-Generics-and-Type-Constraints

interface Printable def display as str

class Dog implements Printable def display as str return "Woof!"

class Cat implements Printable def display as str return "Meow!"

class Printer shared def print_item(item as Printable) print item.display()

class Main shared def main var dog = Dog() var cat = Cat()

Printer.print_item(dog) # Output: Woof! Printer.print_item(cat) # Output: Meow!

Here, Printer.print_item accepts any type that implements Printable. This is a constraint: "T must implement interface Printable".

More advanced: constraints on generic methods:

// file: 13_generic_constraints_advanced.zbr

// teaches: constraints in generic methods // chapter: 13-Generics-and-Type-Constraints

interface Comparable def compare_to(other as this) as int

class ComparableList(T) var items as List(T) = List()

def add(item as T) items.add(item)

def find_max as T? if items.count() == 0 return nil var max = items.at(0) var i = 1 while i < items.count() var item = items.at(i) # Here, T must implement Comparable if item.compare_to(max) > 0 max = item i = i + 1 return max

class Main shared def main var list = ComparableList(int)() list.add(10) list.add(5) list.add(20) var max = list.find_max() if max != nil print "Max: ${max}"


Real World: Generic Cache

A practical example: a cache that works with any type:

// file: 13_generic_cache.zbr

// teaches: realistic generic class // chapter: 13-Generics-and-Type-Constraints

class Cache(K, V) var data as HashMap(K, V) = HashMap() var max_size as int

def init(max_size as int) this.max_size = max_size

def put(key as K, value as V) if data.count() >= max_size and not data.contains(key) # Evict first key (simplistic LRU simulation) # In real code, track access order pass data.put(key, value)

def get(key as K) as V? if data.contains(key) return data.fetch(key) return nil

def clear data = HashMap()

class Main shared def main var cache = Cache(str, int)() cache.init(3) cache.put("a", 1) cache.put("b", 2) cache.put("c", 3)

var val = cache.get("b") if val != nil print "Got: ${val}"


Common Mistakes

Mistake 1: Forgetting to Instantiate Generic Parameters

// WRONG

var box = Container() # Error: T not specified box.set(42)

// CORRECT var box = Container(int)() box.set(42)

Mistake 2: Mixing Types in a Generic Container

// WRONG

var box = Container(int)() box.set("hello") # Error: Expected int, got str

// CORRECT var box = Container(str)() box.set("hello")

Mistake 3: Using Constraints Incorrectly

// WRONG - method doesn't actually require Comparable

def find_max(items as List(T)) as T var max = items.at(0) var item = items.at(1) if item > max # Error: > not defined for all T max = item return max

// CORRECT - either don't use >, or require Comparable interface def find_max(items as List(T)) as T var max = items.at(0) for item in items if item.toString() > max.toString() # Convert to string for comparison max = item return max

Mistake 4: Type Erasure at Runtime

// DANGER - at runtime, type information is lost

def process(items as List(T)) for item in items if item isa int # This may not work as expected print item + 10

In Zebra, type parameters are erased during code generation to Zig. Use interfaces to encode types you need at runtime.


Exercises

Exercise 1: Generic Stack

Implement a Stack(T) class with push, pop, and is_empty methods:

Solution

class Stack(T)

var items as List(T) = List()

def push(value as T) items.add(value)

def pop as T? if items.count() == 0 return nil var value = items.at(items.count() - 1) # Remove last item (simplified - no remove method) return value

def is_empty as bool return items.count() == 0

class Main shared def main var stack = Stack(int)() stack.push(1) stack.push(2) stack.push(3)

var val = stack.pop() if val != nil print "Popped: ${val}"

Exercise 2: Generic Filter Function

Write a function that filters a list based on a predicate (function that returns bool):

Solution

class ListUtils

shared def filter(items as List(T), predicate as T -> bool) as List(T) var result as List(T) = List() for item in items if predicate(item) result.add(item) return result

class Main shared def main var numbers as List(int) = List() numbers.add(1) numbers.add(2) numbers.add(3) numbers.add(4)

var is_even as T -> bool = { x in x % 2 == 0 } var evens = ListUtils.filter(numbers, is_even)

for e in evens print e

Exercise 3: Generic Wrapper with Validation

Create a ValidatedBox(T) that only accepts values passing a validation function:

Solution

class ValidatedBox(T)

var item as T? var validator as T -> bool

def init(validator as T -> bool) this.validator = validator

def set(value as T) as bool if validator(value) item = value return true return false

def get as T? return item

class Main shared def main var age_box = ValidatedBox(int)() age_box.init({ x in x >= 0 and x <= 150 })

if age_box.set(25) print "Valid age: ${age_box.get()}"

if not age_box.set(200) print "Invalid age"


Key Takeaways

- Generics enable type-safe reusable code — Write once, use with many types - Type parameters are like function parametersContainer(T) where T is filled in at use time - Collections are genericsList(T), HashMap(K, V) work with any type - Constraints limit generics — Use interfaces to require specific capabilities - Type erasure happens at compile time — Don't rely on runtime type information


Next Steps

- → 14-Contracts — Express what generics must provide - → 15-Pipelines — Chain generic operations cleanly - → Project 1 — Use generics in real code


Generics are the bridge between flexibility and safety. Master them, and your code scales.