Objects & Data Modelling
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
| Question | object (value) | ref object |
|---|---|---|
| What does assignment do? | Copies every field | Copies the reference: both names see one object |
| Where does it live? | Wherever the variable lives | Heap, managed by ARC/ORC |
| Can it be nil? | No — nil checks are unnecessary | Yes — check before use |
| Typical use | Small records: points, configs, results | Graphs, 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.