Exercise 5 in Chapter 13 asked you to define Integer_Stack, String_Stack, and Real_Stack alongside each other. If you did it, you noticed something uncomfortable: the three classes are identical except for the element type. Every method has the same structure; only the type annotations differ. Any bug fixed in one must be fixed in all three. Any new method added to one should be added to all three.
This is exactly the problem that generic classes solve. A generic class is parameterised by a type: you write the class once, and the type is supplied when the class is used. Stack[Integer], Stack[String], and Stack[Real] are all the same class, instantiated with different type arguments.
This is also how Nex’s standard collections work. Array[T] and Set[T] each take one type argument, and Map[K, V] takes two. Once you understand Stack[G], you understand the core idea behind the standard collection library as well.
The type parameter is declared in square brackets after the class name:
nex> class Stack [G]
create
make() do
items := []
end
feature
items: Array[G]
push(value: G) do
items.add(value)
end
pop(): G do
result := items.get(items.length - 1)
items.remove(items.length - 1)
end
peek(): G do
result := items.get(items.length - 1)
end
is_empty(): Boolean do
result := items.is_empty
end
size(): Integer do
result := items.length
end
end
G is the type parameter — a placeholder for whatever type will be used when the class is instantiated. items is an Array[G]; push takes a G; pop and peek return a G. Everything that was Integer in the original Stack is now G.
The type parameter name is a convention. Single uppercase letters are common: G for a generic element, T for a type, K and V for key and value. The name does not matter — what matters is that it is used consistently throughout the class.
When creating an instance, supply the concrete type in square brackets:
nex> let int_stack := create Stack[Integer].make
nex> int_stack.push(10)
nex> int_stack.push(20)
nex> int_stack.push(30)
nex> int_stack.pop
30
nex> let str_stack := create Stack[String].make
nex> str_stack.push("hello")
nex> str_stack.push("world")
nex> str_stack.peek
"world"
Stack[Integer] is a stack whose element type is Integer. Stack[String] is a stack whose element type is String. Both are produced by the same class definition — only the type argument differs.
Nex enforces type safety: pushing an Integer onto a Stack[String] is a type error caught before the program runs. The generic mechanism provides both reuse and safety.
A class can have more than one type parameter:
nex> class Pair [F, S]
create
make(first_val: F, second_val: S) do
first := first_val
second := second_val
end
feature
first: F
second: S
head: F do
result := first
end
tail: S do
result := second
end
describe(): String do
result := "(" + first.to_string + ", " + second.to_string + ")"
end
end
nex> let p1 := create Pair[String, Integer].make("age", 30)
nex> p1.head
"age"
nex> p1.tail
30
nex> let p2 := create Pair[Real, Boolean].make(3.14, true)
nex> p2.describe
"(3.14, true)"
Pair[F, S] holds a value of type F and a value of type S. The two types are independent — Pair[String, Integer], Pair[Real, Boolean], and Pair[String, String] are all valid instantiations.
Sometimes a generic class needs to call methods on its type parameter — and not all types support all methods. If Stack needed to sort its elements, G would need to support comparison. You cannot sort arbitrary types; you can only sort types that implement Comparable.
Type constraints restrict which types can be used as a type argument. The constraint is written with ->:
nex> class Sorted_List [G -> Comparable]
create
make() do
items := []
end
feature
items: Array[G]
insert(value: G) do
items.add(value)
items := items.sort
end
max(): G do
result := items.get(items.length - 1)
end
min(): G do
result := items.get(0)
end
size(): Integer do
result := items.length
end
end
[G -> Comparable] means: G can be any type that implements Comparable. Inside the class, Nex knows that G values can be compared, so items.sort — which requires Comparable elements — is valid.
nex> let nums := create Sorted_List[Integer].make
nex> nums.insert(5)
nex> nums.insert(2)
nex> nums.insert(8)
nex> nums.insert(1)
nex> nums.min
1
nex> nums.max
8
Attempting create Sorted_List[Array[Integer]].make would be a type error at instantiation, because Array[Integer] does not implement Comparable.
The built-in constraints available in Nex include: - Comparable — supports ordering (<, <=, >, >=) - Hashable — can be used as a map key
Type constraints and multiple parameters combine naturally:
nex> class Dictionary [K -> Hashable, V]
create
make() do
entries := {}
end
feature
entries: Map[K, V]
set(key: K, value: V) do
entries.set(key, value)
end
get(key: K): V do
result := entries.get(key)
end
try_get(key: K, default: V): V do
result := entries.try_get(key, default)
end
contains_key(key: K): Boolean do
result := entries.contains_key(key)
end
size(): Integer do
result := entries.size
end
end
K must be Hashable because map keys require hashing. V is unconstrained — values can be any type. This mirrors the design of the built-in Map type, which is itself a generic class with exactly these constraints.
nex> let dict := create Dictionary[String, Integer].make
nex> dict.set("apples", 5)
nex> dict.set("oranges", 3)
nex> dict.get("apples")
5
nex> dict.try_get("bananas", 0)
0
A generic class can inherit from another class, and a concrete class can inherit from an instantiated generic:
nex> class Bounded_Stack [G] inherit Stack[G]
create
make(max: Integer) do
super.make
max_size := max
end
feature
max_size: Integer
is_full(): Boolean do
result := size = max_size
end
push(value: G) do
if not is_full then
super.push(value)
end
end
end
Bounded_Stack[G] inherits from Stack[G] and adds a max_size field and an is_full check. The push override silently ignores pushes when the stack is full (a real implementation might signal this — we will see how with contracts in Part V).
super.make and super.push(value) are the super keyword from Chapter 14: because Bounded_Stack[G] inherits from exactly one class, super unambiguously means Stack[G]. super.make in the constructor runs Stack[G]’s own constructor first, so items is initialised before max_size is set. super.push(value), inside the if not is_full guard, is what actually appends the value onto the underlying array — the override’s only job is deciding whether that call happens at all.
nex> let s := create Bounded_Stack[Integer].make(3)
nex> s.push(1)
nex> s.push(2)
nex> s.push(3)
nex> s.push(4) -- ignored: stack is full
nex> s.size
3
By now you have used Array[T] and Map[K, V] throughout. They are generic classes built into the language: Array[Integer], Array[String], and Array[Real] are all instances of the same Array class with different type arguments, and Map[String, Integer] and Map[Integer, String] are both instances of Map with different key and value types.
Nex also provides a built-in Set[T] class. A set stores unique values of one element type. The literal syntax is #{...}:
nex> let seen: Set[Integer] := #{1, 2, 3}
nex> seen.contains(2)
true
nex> seen.union(#{3, 4})
#{1, 2, 3, 4}
nex> let empty_names: Set[String] := #{}
nex> empty_names.is_empty
true
The # matters. {} is an empty map; #{} is an empty set.
This is the same generic pattern you have seen all chapter:
Array[T] has one element type parameterSet[T] has one element type parameterMap[K, V] has two type parameters: one for keys and one for valuesBecause these are generic classes, their methods are defined once and then reused for any valid type arguments. add, get, remove, contains, and sort are defined once on Array[T]. contains, union, intersection, and difference are defined once on Set[T]. get, set, try_get, and contains_key are defined once on Map[K, V].
The type arguments explain why the typechecker knows what operations are valid. sort works on Array[Integer] because Integer is Comparable. It would not work on an Array[Map[String, Integer]], because maps are not comparable. Similarly, map keys require K -> Hashable, which is why Map[K, V] constrains its key type.
The generic mechanism also explains why across can infer loop variable types automatically. If numbers has type Array[Integer], the loop variable is inferred as Integer. If seen has type Set[Integer], the loop variable is also inferred as Integer:
nex> across seen as n do
print(n + 10)
end
11
12
13
Understanding that the standard collections are generic classes clarifies the whole type system. Array[Integer], Set[String], and Map[String, Real] are not magical special cases. They are ordinary instances of generic classes, following exactly the same ideas as Stack[Integer], Pair[String, Integer], or Sorted_List[Integer].
If the standard collections are ordinary classes, nothing about across can be reserved for them — and it is not. Chapter 11 left this promise open: across works with any object that knows how to produce a cursor, a small helper that visits the elements one at a time. By giving a class a cursor method you make your own type iterable, so client code can loop over it with the same across it uses for an array.
A cursor is an object that implements four features — the Cursor protocol:
start — position the cursor at the first elementitem — return the element at the current positionnext — advance to the following elementat_end — report whether the traversal is finishedWhen you write across some_object as x do … end, Nex calls some_object.cursor to obtain a cursor, then drives that cursor: it calls start once, and repeats item / next until at_end becomes true. The built-in collections work exactly this way — an array hands back an ArrayCursor, a map a MapCursor — which is why one loop form works uniformly across all of them.
Consider an Interval — an inclusive range of integers — that does not store its values in an array at all, but generates them on demand. First the cursor that walks the range:
nex> class Interval_Cursor
create
make(lo, hi: Integer) do current := lo last := hi end
feature
current: Integer
last: Integer
start() do end
item(): Integer do result := current end
next() do current := current + 1 end
at_end(): Boolean do result := current > last end
end
The cursor holds its own position in current. It starts already on the first element, so start has nothing to do; item reads the current value; next moves forward; and at_end is true once it has stepped past last. Now the Interval itself, whose only job is to hand out a fresh cursor:
nex> class Interval
create
make(lo, hi: Integer) do low := lo high := hi end
feature
low: Integer
high: Integer
cursor(): Interval_Cursor
do result := create Interval_Cursor.make(low, high) end
end
That is all it takes. An Interval is now a first-class iterable:
nex> let week := create Interval.make(1, 7)
nex> across week as day do
print(day)
end
1
2
3
4
5
6
7
Each cursor call returns a new, independent cursor, so the same Interval can be iterated more than once, and even nested inside another loop over itself, without the traversals interfering.
One detail carries over from Section 11.2. Because a user-defined cursor can yield elements of any type, the loop variable for a custom iterable comes through as Any. When you only print it, that is fine — print and to_string accept Any. But to use it as a number you narrow it with convert, exactly as you would a value pulled from a map of Any:
nex> let total := 0
nex> across (create Interval.make(1, 100)) as n do
if convert n to value: Integer then
total := total + value
end
end
nex> total
5050
The lesson generalises beyond ranges. Any class that models a collection — a ring buffer, a linked list, a tree with a chosen traversal order — becomes usable with across the moment it can produce a cursor. The iteration protocol is the meeting point: clients write one familiar loop, while each type decides privately how its elements are stored and produced. It is the same bargain generics offer, struck at the level of behaviour rather than types: Stack[G] lets one class serve every element type, and the Cursor protocol lets one loop serve every collection.
A common pattern in robust code is a result type that holds either a successful value or an error description — without raising an exception. Here the success value varies, while the error is always a String, so a one-parameter generic is enough:
nex> class Result [V]
create
success(val: V) do
value := val
error := nil
ok := true
end
failure(msg: String) do
value := nil
error := msg
ok := false
end
feature
value: ?V
error: ?String
ok: Boolean
is_ok(): Boolean do
result := ok
end
describe(): String do
if ok then
if value /= nil then
result := "Success: " + value.to_string
else
result := "Error"
end
elseif error /= nil then
result := "Error: " + error
else
result := "Error"
end
end
end
nex> function safe_divide(a, b: Real): Result[Real]
do
if b = 0.0 then
result := create Result[Real].failure("division by zero")
else
result := create Result[Real].success(a / b)
end
end
nex> safe_divide(10.0, 2.0).describe
"Success: 5.0"
nex> safe_divide(10.0, 0.0).describe
Error: division by zero
Result[V] has two named constructors — success and failure — making the two cases explicit. The caller can check is_ok and handle each case without catching an exception. This pattern — sometimes called a result type or either type — appears in many modern languages and libraries. Writing it yourself as a generic class in Nex is a good exercise in combining what this chapter has covered.
Nex ships one, so you do not have to: intern data/Result brings in Result[T, E], along with its two variants and a set of combinator functions. Its design differs from the version above in a few ways worth noting. The error type is its own parameter E rather than always String, so Result[Real, String] and, say, Result[Real, Parse_Error] — with a class of your own describing what went wrong — are equally valid instantiations. Success and failure are separate classes, Ok[T, E] and Err[T, E], rather than one class with a flag — matched with match r of when Ok as ok then … when Err as er then … end instead of branching on is_ok. Querying and unwrapping — is_ok(), is_err(), unwrap_or(fallback) — are methods, but transforming a result — result_map, result_and_then, result_map_err — are free functions, since each introduces a type parameter a method has no way to add. Construction infers the type arguments it can from the value passed and leaves the rest as Any: create Ok.make(42) is Ok[Integer, Any], which still assigns to a Result[Integer, String]-typed variable. In real code, reach for this built-in Result rather than the hand-rolled one above, which exists here to practice generics and constructors, not as a pattern to repeat.
class Name [T]create Stack[Integer].makeclass Pair [F, S][G -> Comparable] requires G to implement Comparable; [K -> Hashable] requires hashability for use as a map keyclass Bounded_Stack [G] inherit Stack[G]Array[T], Set[T], and Map[K, V] are generic classes; Set literals use #{...}, and understanding these collection types explains why element types are inferred and why operations work uniformly across typesacross by giving it a cursor method that returns an object implementing the Cursor protocol — start, item, next, at_end — which is how the built-in collections work too; narrow the Any loop variable with convert when you need its concrete type1. The Stack[G] class has implicit preconditions: pop and peek require the stack to be non-empty. Add a require comment to each method stating the precondition. Then test what happens when you call pop on an empty stack.
2. Define a generic class Box [T] with a single field value: T, a constructor make(v: T), and methods get(): T and set(v: T). Then define a Logged_Box [T] inherit Box[T] that also keeps a change_count: Integer field, incrementing it each time set is called. Add a changes(): Integer method.
3. Define a generic class Range [G -> Comparable] with fields low: G and high: G, a constructor make(l, h: G), and methods contains(value: G): Boolean (returns true if low <= value <= high) and overlaps(other: Range[G]): Boolean. Test with integer and real ranges.
4. The Result[V] class in Section 15.9 has value: ?V as a detachable field. Why is ?V needed rather than V? What would happen in the failure constructor if value were not detachable?
5.* Define a generic Queue [G] class backed by an Array[G], with methods enqueue(value: G), dequeue(): G, front(): G, is_empty(): Boolean, and size(): Integer. Then define a Priority_Queue [G -> Comparable] that inherits Queue[G] and overrides enqueue so that elements are always inserted in sorted order (smallest at the front). Verify that dequeuing from a Priority_Queue[Integer] after inserting [5, 2, 8, 1, 9] produces the elements in ascending order.
6. Following the Interval example from Section 15.8, write a class Countdown whose constructor takes a positive integer n and that, when iterated with across, yields n, n-1, …, 1. Give it a cursor implementing start, item, next, and at_end, and a cursor method that returns a fresh one. Verify that across (create Countdown.make(5)) as k do print(k) end prints 5 down to 1, and that iterating the same object twice produces the full sequence both times.
7.* Make the Stack[G] class from Section 15.1 iterable from top to bottom by adding a cursor method, without exposing its underlying items array. Then write a loop that uses convert to total the elements of a Stack[Integer], and explain why the loop variable arrives as Any.