Tuples, Variants & Enums

Not every group of values deserves a type declaration. Tuples group values by position, enumerations give names to a small set of states, and object variants express a choice between alternatives that the compiler can check for exhaustiveness. Together they cover most modelling needs without inheritance.

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.