Generics & Type Classes
Generic Procedures and Types
Bracket the type parameter after the name: proc f[T](...). The compiler infers T from the arguments in almost every case, so generic code rarely needs explicit instantiation.
Figure 1 — the definition is written once; the compiler produces a separate, fully typed copy for every type argument used.
One Implementation, Many Types
proc first[T](items: openArray[T]): T =
## openArray accepts both arrays and sequences without copying.
items[0]
echo first([10, 20, 30]) # 10 — T inferred as int
echo first(@["a", "b"]) # a — T inferred as string
# echo first([]) # an empty literal gives the compiler nothing to infer
proc swap[T](a, b: var T) =
let temp = a
a = b
b = temp
var x = 1
var y = 2
swap(x, y) # works for any type, not just int
echo x, " ", y # 2 1
Generic Types
type
Stack[T] = object # a container specialized on demand
items: seq[T]
proc push[T](s: var Stack[T], value: T) =
s.items.add(value)
proc pop[T](s: var Stack[T]): T =
## Caller must check 'isEmpty' first; popping an empty stack is a defect.
result = s.items[^1]
s.items.setLen(s.items.len - 1)
proc isEmpty[T](s: Stack[T]): bool = s.items.len == 0
var numbers: Stack[int] # T = int for this variable, fixed at compile time
numbers.push(7)
numbers.push(9)
echo numbers.pop() # 9 — last in, first out
echo isEmpty(numbers) # false
var words: Stack[string] # the same code, a different element type
words.push("go")
echo words.pop() # go
Type Constraints and Concepts
An unconstrained T accepts everything, which is rarely what you want. Nim's constraint syntax after the colon names either a concrete type, a type class from the standard library, or a concept you define yourself.
Constraining the Type Parameter
proc total[T: SomeNumber](items: openArray[T]): T =
## 'SomeNumber' is a type class covering int, float and their sized variants.
## Without the constraint, 'result += item' would not compile for every T.
for item in items:
result += item
echo total([1, 2, 3]) # 6 — T is int
echo total([1.5, 2.5]) # 4.0 — T is float
# echo total([true, false]) # compile error: bool is not a SomeNumber
proc factorial(n: Natural): int =
## 'Natural' is a range type (int >= 0), so no runtime check is needed here.
result = 1
for i in 2 .. n:
result *= i
echo factorial(5) # 120
# let bad: Natural = -1 # RangeDefect at runtime if the value is dynamic
Concepts: Structural Contracts
A concept states the operations a type must support. Nothing is registered or inherited: if the type can do what the concept lists, it matches.
type
Printable = concept x # 'concept x' names the parameters it describes
$x is string # any type whose '$' yields a string matches
proc describe[T: Printable](value: T): string =
"value = " & $value # the concept guarantees '$' exists
echo describe(42) # value = 42
echo describe(1.5) # value = 1.5
# A type without a '$' operator is rejected at compile time, with the reason.
Compile-Time Branching with 'when'
Inside a generic body, when runs at compile time and only the surviving branch is compiled for each instantiation. That is how library code specializes behaviour without paying for runtime dispatch.
proc double[T](value: T): T =
when T is SomeInteger: # exact integer arithmetic
value * 2
elif T is SomeFloat: # floating point path
value * 2.0
else:
{.error: "double supports numbers only".} # rejected while compiling
echo double(21) # 42
echo double(1.5) # 3.0
# echo double("x") # compile-time error from the {.error.} branch
Generics in Practice
Most generic code you will write comes from three places: standard-library type classes in parameter lists, containers you define once and reuse, and the deliberate choice between generics and templates for compile-time abstraction.
Type Classes from the Standard Library
import std/algorithm
var data = @[3, 1, 2]
data.sort # generic over any comparable element type
echo data # @[1, 2, 3]
proc total(values: varargs[int]): int =
## 'varargs' accepts any number of arguments of one type.
for value in values:
result += value
echo total(1, 2, 3) # 6
echo total() # 0 — zero arguments is valid
# echo total(1, "2") # compile error: the elements must all be int
Generic or Template?
- Generic (
proc f[T]): a real procedure with a type parameter. It is type-checked at the definition, debuggers see it, and recursion works. Prefer this by default. - Template (
template t(x: untyped)): textual expansion with hygiene — the argument is substituted, not evaluated first. Use it for control-flow helpers, logging wrappers and zero-cost sugar, never as a substitute for a procedure. - Rule of thumb: if you need the argument's value, use a generic; if you need its syntax (an expression that must not be evaluated, like the body of a logger), use a template or a macro.
Review Checklist
- Add the narrowest constraint that still fits your callers —
SomeNumberbeforeSomeInteger,SomeIntegerbeforeint. - Prefer
openArray[T]overseq[T]for read-only parameters: it accepts arrays, sequences and slices without copying. - Keep generic bodies short. A generic function that is hard to read is hard to debug, because errors are reported at the instantiation site.
- Write one concrete test per instantiation you actually ship — the compiler proves the types fit, not that the results are right.