Tuples, Variants & Enums
Tuples
A tuple is a fixed group of values that may have different types. Unlike an object it needs no type declaration, which makes it ideal for returning several results at once.
Named and Unnamed Tuples
let pair = ("ada", 36) # unnamed tuple: fields accessed as [0], [1]
echo pair[0], " is ", pair[1]
let person = (name: "linus", age: 54) # named tuple: fields by name
echo person.name, " is ", person.age
# A tuple type is structural: same field names and types means same type.
proc birthYear(p: tuple[name: string, age: int]): int = 2026 - p.age
echo birthYear(person) # 1972
# Tuples destructure directly in the declaration.
let (name, age) = person
echo name, "/", age
# Procedures that must report success return a tuple instead of a magic value.
proc safeDivide(a, b: int): tuple[ok: bool, value: int] =
if b == 0: (false, 0)
else: (true, a div b)
echo safeDivide(10, 2) # (ok: true, value: 5)
echo safeDivide(1, 0)[0] # false — check the flag, never the value alone
Tuples in Collections
import std/algorithm
var people = @[(name: "ada", age: 36), (name: "linus", age: 54)]
# Tuple comparison is lexicographic and built in, so sorting needs no comparator.
people.sort()
echo people # sorted by name, then age
for (n, a) in people: # destructuring works inside a for loop
echo n, " -> ", a
Enumerations
An enum names a closed set of states. The compiler knows every member, which is what makes case exhaustive and turns a forgotten state into a compile error rather than a silent branch.
Members, Values and Conversion
type
Level = enum lLow, lMid, lHigh # ordinals 0, 1, 2
Status = enum stOk = 200, stFound = 302, stError = 500 # explicit values
echo ord(lHigh) # 2 — the underlying ordinal
echo $stFound # "stFound" — '$' yields the member name
echo lLow < lMid # true — enums compare by ordinal
# parseEnum converts text back to a member; it raises on unknown input.
import std/strutils
let parsed = parseEnum[Level]("lMid")
echo parsed, " at ", ord(parsed) # lMid at 1
# A set of an enum is a natural permission or option bundle.
var seen: set[Level] = {}
seen.incl(lMid)
echo seen.len, " ", lLow in seen # 1 false
Driving Logic with case
proc cost(level: Level): int =
case level # no 'else': all three values are handled
of lLow: 10
of lMid: 50
of lHigh: 200
echo cost(lMid) # 50
proc httpText(s: Status): string =
case s
of stOk: "success"
of stFound: "redirect"
of stError: "failure" # the compiler verifies nothing is missing
echo httpText(stFound)
Object Variants (Tagged Unions)
An object variant stores a tag field plus the fields that only exist for one tag. It is the memory-efficient way to express "one of these shapes", and case is how you open it safely.
Declaring the Tagged Union
import std/strformat
type
NodeKind = enum nkNumber, nkText # the tag: which alternative is active
Node = object
case kind: NodeKind # 'case' inside an object = the tag
of nkNumber:
number: float
of nkText:
text: string
let a = Node(kind: nkNumber, number: 3.5) # only the matching fields may be set
let b = Node(kind: nkText, text: "hello")
# let bad = Node(kind: nkNumber, text: "x") # compile error: field not in this branch
echo a.number, " ", b.text # 3.5 hello
Matching the Variant
proc render(n: Node): string =
case n.kind # read the tag first, then the branch fields
of nkNumber: &"number({n.number})"
of nkText: &"text({n.text})"
let nodes = @[Node(kind: nkNumber, number: 2.0), Node(kind: nkText, text: "x")]
for n in nodes:
echo render(n) # number(2.0) text(x)
# A recursive variant models a tree: each branch refers back to the same type.
type Expr = ref object
case isLeaf: bool
of true:
value: int
of false:
left, right: Expr
proc eval(e: Expr): int =
if e.isLeaf: e.value # the leaf payload is only valid here
else: eval(e.left) + eval(e.right)
echo eval(Expr(isLeaf: false, left: Expr(isLeaf: true, value: 2),
right: Expr(isLeaf: true, value: 5))) # 7
Practice
Define a tagged union for a JSON-like value (number, text, list) and write one procedure that renders any of them to a string. The exercise shows why the tag must be read before the payload, and why an exhaustive case is safer than a chain of ifs. Next: Generics & Type Classes.