Passive Objects
Part of: Reef Language Reference Last reviewed on version: 0.9.0 Status: Implemented (0.9)
Overview
A passive object is a heap-allocated class instance with single
inheritance and virtual methods. It is not an Active Object: there is
no monitor, no run() thread, and no await. The exclusive / shared
markers on a passive method are a requirement on the caller's owner
context, not a lock the object takes.
| Kind | Heap | Inheritance | Concurrency |
|---|---|---|---|
| Struct | optional (new or by-value literal) |
none | none |
| Passive object | always (new Class(...)) |
single, virtual methods | owner-context discipline |
| Active Object | always | none (an AO may own objects) | monitor; optional run() |
Use a struct for a data record. Use a passive object when you want a hierarchy and virtual dispatch (widgets, devices, AST nodes). Use an Active Object when the thing is the concurrency boundary.
See 045_ACTIVE_OBJECTS.md for the monitor model. Traits on objects are in Traits on objects below and in 050_TRAITS.md.
object is a reserved word. extends and override are contextual
(special only in an object declaration). inherited and typecase are
reserved.
Declaration
Fields first, then methods. Field access is always self.x — there is
no implicit field lookup, so a local cannot shadow a field.
object Point
x: int
y: int
init(x: int, y: int)
self.x = x
self.y = y
end init
shared fn sum(): int
return self.x + self.y
end sum
end Point
proc main()
let p = new Point(3, 4)
println("${p.sum()}")
end main
Every method is shared (read-safe) or exclusive (may mutate self).
init and finalize take no marker.
Construction
new Class(args) allocates, then runs init. A class with no init
zero-initializes (new Class()). init is not virtual: each class
has its own. A derived init must call inherited init(...) as its
first statement.
object Counter
n: int
init()
self.n = 0
end init
exclusive proc bump()
self.n = self.n + 1
end bump
shared fn get(): int
return self.n
end get
end Counter
proc main()
let c = new Counter()
c.bump()
println("${c.get()}")
end main
Inheritance and virtual dispatch
extends names one base class. Methods are virtual by default.
override is mandatory on a redefinition and rejected when
nothing of that name exists to override. The exclusive/shared marker
is part of the signature and may not change.
inherited m(args) statically calls the defining class's base's
implementation of m (not the dynamic receiver's).
A Base-typed reference still dispatches to the override:
object Widget
x: int
y: int
init(x: int, y: int)
self.x = x
self.y = y
end init
shared proc paint()
println("Widget(${self.x},${self.y})")
end paint
end Widget
object Button extends Widget
label: string
init(x: int, y: int, label: string)
inherited init(x, y)
self.label = label
end init
override shared proc paint()
inherited paint()
println(" label=${self.label}")
end paint
end Button
proc main()
let b = new Button(1, 2, "ok")
let w: Widget = b
w.paint()
end main
A Derived value inhabits any Base slot: bindings, parameters, returns,
call arguments, and array elements. Array types stay invariant
([Button] is not a [Widget]).
== / != on objects is reference identity. Ordering (<, >) is
rejected.
is, as, and typecase
w is Button walks the runtime class chain. w as Button aborts the
process on mismatch (safe Reef has no nil for user types). Prefer
typecase when several arms are needed — the binder is the narrowed
view. First matching arm wins; a later arm whose type is equal to or a
subclass of an earlier arm is a type error (statically unreachable).
object Widget
n: int
init(n: int)
self.n = n
end init
end Widget
object Button extends Widget
init(n: int)
inherited init(n)
end init
end Button
proc tag(w: Widget)
typecase w
Button b =>
println("button ${b.n}")
end
else =>
println("other ${w.n}")
end
end typecase
end tag
proc main()
let b = new Button(1)
let w: Widget = b
if w is Button
let d = w as Button
println("as ${d.n}")
end if
tag(b)
tag(new Widget(2))
end main
type_name / type_id report the dynamic class.
Traits on objects
impl Trait for Class works the same as for a struct. Subclasses
inherit the base's impls for method syntax; a subclass's own impl
wins for its static type. Trait methods are statically dispatched to
the impl target — they are not virtual.
The pattern that does give you "one impl, per-class behaviour" is delegate-to-virtual: the class method is the virtual; the trait impl forwards to it. The two names must differ. An own method and an impl method of the same name on the same class is a type error (one name, one method).
trait Drawable
fn draw(): string;
end Drawable
object Widget
n: int
init(n: int)
self.n = n
end init
shared fn describe(): string
return "widget"
end describe
end Widget
object Button extends Widget
init(n: int)
inherited init(n)
end init
override shared fn describe(): string
return "button"
end describe
end Button
impl Drawable for Widget
fn draw(): string
return self.describe()
end draw
end impl
fn show[T](x: T): string where T: Drawable
return x.draw()
end show
proc main()
let b = new Button(1)
let w: Widget = b
println("${b.draw()} ${w.draw()} ${show(b)}")
end main
b.draw() and w.draw() both run Widget's impl. That body calls
virtual describe(), so a Button receiver prints "button". show(b)
is the same impl reached through a where T: Drawable bound — a
subclass inhabits the bound when a base implements the trait.
Do not write impl Drawable for Widget with a method also named
describe. Rename the trait method, or drop the impl and call the
virtual directly.
A working tree that combines inheritance, virtuals, typecase, and this
trait pattern is examples/widget_tree.reef.
finalize
Optional. Runs on reclaim, not virtually. After a derived
finalize returns, the base's finalize (if any) runs automatically —
do not write inherited finalize(), and do not mark finalize
override. A finalizer must not allocate (it runs while the heap lock
is held) and must not touch owned object graphs (external resources
only).
object Handle
fd: int
init(fd: int)
self.fd = fd
end init
finalize()
end finalize
end Handle
proc main()
let h = new Handle(3)
end main
What 0.9 does not do
- No
dyn Trait. Traits stay compile-time (monomorphizedwherebounds). Runtime dispatch is the class vtable. - No generic objects.
object Stack[T]andextends Container[int]are type errors. - No object-typed
spawnarguments. A spawned thread is never the owner. - No multiple inheritance, and no inheritance into or out of Active Objects. An AO may own a passive graph.
- No qualification of colliding trait methods. Two impls providing the same method name on one type is a type error; rename.
Owner-check harness
Owner-check probes are opt-in (reefc --owner-harness). The runtime
stamps each object at new with the current owner (innermost held Active
Object, else the allocating thread) and aborts on a mismatch at exclusive
method entry, shared method entry (read), and field writes. Finalize-time
access always fires. Default builds emit no probes. Pins:
reef-compiler/examples/test_owner_*.reef.
unsafe does not relax this rule: a cross-owner method call or field
write is still a cross-owner access. The harness is how you catch it.