Objects & Data Modelling

An object in Nim is a plain record with named fields and no hidden machinery. Understanding the difference between a value object and a ref object — one is copied on assignment, the other is shared — decides how your programs behave far more than any keyword you add to a type.

Defining and Using Objects

Declare an object with type, fields with names and types, and construct it with the type name and named field values. Field defaults and derived fields are available when a bare constructor would repeat itself.

Value Objects Are Copied

type
  Point = object
    x, y: float
    label: string = "origin"      # a default value, applied when omitted

var start = Point(x: 0.0, y: 0.0)         # 'label' takes the default
echo start.label                          # origin

var copy = start                          # assignment copies the whole object
copy.x = 5.0
echo start.x, " ", copy.x                 # 0.0 5.0 — the two are independent

# Nested objects compare and copy structurally, so equality is straightforward.
let a = Point(x: 1.0, y: 2.0)
let b = Point(x: 1.0, y: 2.0)
echo a == b                               # true — same field values

Procedures Belonging to the Type

There are no member functions; a procedure whose first parameter is the object type is how you give a type behaviour. Uniform call syntax makes the call site read like a method.

import std/math

type Circle = object
  radius: float

proc area(c: Circle): float = PI * c.radius * c.radius
proc `<`(a, b: Circle): bool = a.radius < b.radius   # enables sorting

let small = Circle(radius: 1.0)
let large = Circle(radius: 4.0)
echo area(large)                 # 50.265... — plain call form
echo large.area()                # same call through uniform call syntax
echo small < large               # true — our operator, not a built-in

Reference Objects

A ref object lives on the heap and is referenced by identity: assignment shares the same object instead of copying it. This is what you want for graphs, caches and any structure where two parts of the program must see the same data.

Sharing Through a Reference

type
  Account = ref object            # 'ref object' allocates on the heap
    owner: string
    balance: float

var joint = Account(owner: "ada", balance: 100.0)   # allocation, no 'new' needed
var alias = joint                  # copies the reference, not the object
alias.balance -= 25.0              # writes through the shared object
echo joint.balance                 # 75.0 — the same account, seen twice

# A reference can be absent; nil is the only value it can hold besides an object.
var pending: Account
echo pending == nil                # true — check before dereferencing
pending = Account(owner: "linus", balance: 0.0)
echo pending.owner                 # linus

Choosing Value or Reference Semantics

Questionobject (value)ref object
What does assignment do?Copies every fieldCopies the reference: both names see one object
Where does it live?Wherever the variable livesHeap, managed by ARC/ORC
Can it be nil?No — nil checks are unnecessaryYes — check before use
Typical useSmall records: points, configs, resultsGraphs, trees, long-lived state

Inheritance, Dynamic Dispatch and Composition

Nim supports object inheritance for the cases that need it — a base type, of for runtime tests, and method for dynamic dispatch — while keeping the default at a single level of modelling.

Base Types and Runtime Tests

type
  Shape = ref object of RootObj      # 'of RootObj' makes the object inheritable
    name: string
  Square = ref object of Shape
    side: float
  Rect = ref object of Shape
    w, h: float

proc area(s: Shape): float =
  if s of Square: Square(s).side * Square(s).side   # 'of' tests the runtime type
  elif s of Rect: Rect(s).w * Rect(s).h
  else: 0.0

let shapes: seq[Shape] = @[Square(name: "sq", side: 2.0), Rect(name: "rc", w: 3.0, h: 4.0)]
for shape in shapes:
  echo shape.name, " area=", area(shape)      # sq area=4.0  rc area=12.0

Methods and Dispatch

When the branch table would grow with every new subtype, declare the operation as a method. Methods dispatch on the runtime type of their first argument, so adding a subtype does not require editing existing code.

type
  Animal = ref object of RootObj
    name: string
  Dog = ref object of Animal
  Cat = ref object of Animal

method speak(a: Animal): string {.base.} = "..."      # the base implementation
method speak(d: Dog): string = "woof"
method speak(c: Cat): string = "meow"

let pets: seq[Animal] = @[Dog(name: "rex"), Cat(name: "mia")]
for pet in pets:
  echo pet.name, " says ", pet.speak()     # rex says woof   mia says meow

# Composition usually beats inheritance: keep the shared part as a field
# instead of a base type, and the type stays free of hidden coupling.
type Engine = object
  cylinders: int
type Car = object
  engine: Engine          # 'has-a' rather than 'is-a'

Practice

Model a small inventory: a value object for each item, a seq of them, and one procedure per operation (add, remove, totalValue) that takes the sequence as its first parameter. Then convert it to ref object and describe which behaviour changed — that comparison is the real lesson. Next: Tuples, Variants & Enums.