Structs
What is a struct
A struct is a lightweight data record: it groups several values under field names, just like the properties of a class, but with the set of fields fixed at declaration time. In exchange for that restriction, fields are stored internally by index in a compact memory block, with no hash tables, which makes a struct several times smaller than a class instance and much faster to create.
Structs do not replace classes: they complement them. JARU offers three ways to group data, each with its own purpose:
| Container | Fields | Ideal use |
|---|---|---|
map | Dynamic, any key at runtime | Data whose structure is not known in advance (JSON, configuration) |
class | Dynamic, with methods, inheritance and encapsulation | Objects with behavior: data + logic |
struct | Fixed, data only | Pure records: points, game entities, sensor readings |
If you need methods, inheritance, or adding properties on the fly, use a class. If you just need to group data — especially if you are going to create many objects on an ESP32 — use a struct.
Declaration
A struct is declared with the struct keyword, followed by its name and the var declarations of its fields:
struct Point
var x = 0
var y = 0
end
Several fields can be declared on the same line, and a field without an initial value is 0 (just like any var in JARU):
struct Enemy
var x = 0, y = 0 // several fields per line
var hp = 100
var name = "orc"
var alive = true
var target // no initial value: it is 0
end
The declaration order of the fields is part of the struct's contract: it determines the order of the arguments when creating instances. Reordering the fields of a struct changes the meaning of existing calls.
Default values: constant literals only
The default values of the fields must be constant literals: numbers (with optional sign), strings, true/false or nil. Any expression produces a compile-time error:
struct Config
var retries = 3 // ok: number
var speed = -1.5 // ok: signed number
var mode = "auto" // ok: string
var debug = false // ok: boolean
var owner = nil // ok: nil
var pos = Point() // ERROR: not a literal
var items = [] // ERROR: not a literal
var seed = clock() // ERROR: not a literal
end
This restriction has two reasons. The first is performance: since defaults are constants, creating an instance is a simple memory copy. The second is safety: if a field could default to [], all instances would share the same list — a classic source of hard-to-find bugs in other languages.
When a field needs to hold a list, a map or another struct, the idiomatic pattern is to assign it after creating the instance, usually in a factory function:
func newEnemy(x, y)
var e = Enemy()
e.x = x
e.y = y
e.target = Point(x, y) // the field IS declared; it is filled here
return e
end
Creating instances
A struct is instantiated by calling it like a function, just like a class. Arguments are positional: they fill the fields in declaration order, and any missing ones take their default value:
var a = Point() // {x: 0, y: 0} all defaults
var b = Point(3, 4) // {x: 3, y: 4} positional
var c = Point(7) // {x: 7, y: 0} partial: the rest to defaults
var d = Point(1, 2, 3) // EXCEPTION: Point only has 2 fields
Field access and sealing
Fields are read and written with dot notation, like the properties of a class:
var p = Point(1, 2)
p.x = 10
p.y = p.x + 5
println(p.x, " ", p.y) // 10 15
The big difference from classes is that a struct is sealed: the set of fields is fixed. Accessing a field that does not exist throws an exception, also when writing:
var e = Enemy()
e.helth = 50 // typo of 'hp': EXCEPTION right away
// Struct 'Enemy' has no field 'helth'
// (fields: x, y, hp, name, alive, target).
In a class, e.helth = 50 silently creates a new property called helth, and the bug does not show up until much later, somewhere else in the program. In a struct, the typo is caught on the exact line where it happens, and the message includes the list of valid fields.
Reference semantics, copy and clone
Like every other object in JARU, assigning a struct copies the reference, not the contents:
var a = Point(1, 2)
var b = a
b.x = 99
println(a.x) // 99: a and b are the same object
To duplicate a struct there are the copy() method (shallow copy: nested containers are shared) and clone() (deep copy: lists, maps and nested structs are duplicated too), with the same contract as lists, maps and instances:
var original = newEnemy(5, 5)
var copied = original.copy() // fields duplicated, target shared
var cloned = original.clone() // everything duplicated, target too
Equality == compares references: two different structs with the same values are not equal, just as [1, 2] == [1, 2] is false.
Nested structs and structs in containers
A field can hold any value: another struct, a list, a map... Only the default values are restricted to literals; runtime contents are free:
struct Segment
var a = nil
var b = nil
end
var s = Segment(Point(1, 2), Point(3, 4))
println(s.a.x) // 1: chained access
var enemies = [newEnemy(0, 0), newEnemy(10, 5)]
println(enemies[1].x) // 10: structs inside lists
Printing a struct shows its fields with their names:
println(Point(3, 4)) // Point{x: 3, y: 4}
Functions in fields (callbacks)
A struct has no methods of its own, but since a field can hold any value, it can hold a function. If you store a function in a field, you can call it with dot notation, just as you would with an instance property:
func onFire(x, y)
println("shot at ", x, ", ", y)
end
struct Button
var label = ""
var onClick = nil // we will store a function here
end
var b = Button()
b.label = "Fire"
b.onClick = onFire // assign the function to the field
b.onClick(100, 50) // call it: prints "shot at 100, 50"
The function stored in the field is invoked as-is: it does not receive the struct as this, because it is not a method of the struct but a regular function that just happens to be reached through a field. This is exactly the same mechanism that already exists for the properties of a class instance.
This makes structs a convenient, lightweight way to build dispatch tables or event handlers: each game entity can carry its own behavior function in a field, without the cost of a class.
If the field does not hold a callable value (a number, for example), calling it throws an exception, just like calling any other non-callable value. And if the name matches no field (nor copy/clone), the call throws has no field or method.
Memory and performance
The reason structs exist is efficiency, especially on the ESP32, where RAM is the scarcest resource. Measured with memUse() creating 10,000 objects:
| Object | As a class | As a struct | Difference |
|---|---|---|---|
| Point (2 fields) | 184 bytes | 36 bytes | 5.1x less |
| Game entity (8 fields) | 440 bytes | 84 bytes | 5.2x less |
For a game with 500 entities of 8 fields, that means going from ~215 KB to ~41 KB: on a board with ~230 KB free, it is the difference between fitting or not fitting.
Instance creation is also faster (around 1.4x compared to a class with a constructor), because no init method runs and no hash tables are built. Field access speed, on the other hand, is practically the same as in a class: the advantage of structs is in memory and creation, not in reads.
What a struct does not have
Deliberately, a struct does not support:
- Methods or inheritance — a struct with behavior is a class: use
class. (You can store a function in a field and call it; see Functions in fields.) - Private fields — all fields are public.
- Adding or removing fields at runtime — for dynamic keys, use a
map. - Iteration with
foreach— a struct is not iterable.
Complete example
struct Bullet
var x = 0, y = 0
var vx = 0, vy = -4
var active = true
end
func updateBullets(bullets)
foreach (var b in bullets)
if (b.active) then
b.x = b.x + b.vx
b.y = b.y + b.vy
if (b.y < 0) then
b.active = false
end
end
end
end
var bullets = []
for (var i = 0; i < 100; i++)
bullets.append(Bullet(i * 3, 240))
end
updateBullets(bullets)
println(bullets[0]) // Bullet{x: 0, y: 236, vx: 0, vy: -4, active: true}
One hundred bullets as structs take a fraction of what they would take as class instances, they are created almost instantly, and a typo in any field is caught on the spot.