- Implementation MI (state + code) via composition + this-adjustment forwarding — embed each base as a subobject, generate a forwarding proc per inherited method.
- Polymorphism MI (heterogeneous dispatch) via per-concrete-type role interfaces — a fat pointer holding the whole object plus a vtable whose thunks cast back to the concrete type.
Crucially the interface value holds the whole object, so two different role-views of the same instance share one pointer → identity is preserved and cross-interface as is trivial. This is the fix for macro #1's fatal flaw.
class Drawable:
pos: Point
method draw(self, ctx)
method move(self, dx, dy)
class Named:
name: string
method setName(self, s)
class Sprite: Drawable, Named: # <- multiple inheritance
extra: int
method draw(self, ctx) = ... # overridetype
Drawable = ref object of RootRef
pos: Point
Named = ref object of RootRef
name: string
Sprite = ref object of RootRef # flat — NOT "of Drawable"
base_Drawable: Drawable # one subobject per base
base_Named: Named
extra: int# inherited, not overridden -> delegate to the embedded base's impl
proc move(self: Sprite, dx, dy: int) =
move(self.base_Drawable, dx, dy)
proc setName(self: Sprite, s: string) =
setName(self.base_Named, s)
# overridden -> Sprite's own body; can still reach the base explicitly
proc draw(self: Sprite, ctx: ptr CTXRender) =
echo "sprite at ", self.base_Drawable.pos
draw(self.base_Drawable, ctx) # explicit base call (no 'super' needed)Construction initializes every base subobject:
proc newSprite(): Sprite =
new result
result.base_Drawable = Drawable(pos: Point(0,0))
result.base_Named = Named(name: "")
result.extra = 0This already delivers full C++-style implementation MI: many bases, shared state, code reuse, overrides, explicit base dispatch. Diamond is unambiguous — you always name the path (self.base_Drawable... vs self.base_Named...).
type
DrawableIVT = object # method set of the Drawable role
draw: proc(self: RootRef, ctx: ptr CTXRender)
move: proc(self: RootRef, dx, dy: int)
DrawableI = object # fat pointer: whole object + per-type vtable
obj: RootRef
vtable: ptr DrawableIVT
# one vtable per concrete type that implements the role;
# thunks cast back to the concrete type and call the (possibly overridden) method
proc spriteDrawableVT(): ptr DrawableIVT =
var v {.global.} = DrawableIVT(
draw: proc(self: RootRef, ctx: ptr CTXRender) = draw(cast[Sprite](self), ctx),
move: proc(self: RootRef, dx, dy: int) = move(cast[Sprite](self), dx, dy))
addr v
proc toDrawable(s: Sprite): DrawableI =
DrawableI(obj: cast[RootRef](s), vtable: spriteDrawableVT())The Named role is emitted symmetrically (NamedI, spriteNamedVT, toNamed). Now the payoff:
var s = newSprite()
let d = s.toDrawable()
let n = s.toNamed()
d.draw(ctx) # dispatches to Sprite.draw (the override)
n.setName("hero")
echo d.obj == n.obj # true — same identity, recoverableBecause both views hold the same RootRef, navigation between roles is a pointer comparison. Add a tiny type-id and you get as:
type Any = object
obj: RootRef
typeID: TypeID # which concrete type this is
vtable: ptr RoleTable # table of role-name -> vtable for this type
proc as(a: Any, Role: typedesc): Option[Role] =
if a.typeID in Role.implementors: # consult a compile-time/RTTI registry
some(castRole(a, Role)) else: none- No layout coupling between roles (each face is independent) → no diamond problem in the polymorphic layer.
- Real state + code reuse in the composition layer → genuine implementation MI, not just contracts.
- Identity preserved because the interface handle points at the whole object, not an erased slice — the one thing macro #1 got wrong.
- Virtual dispatch follows overrides because the per-type vtable thunk casts to the concrete type and calls the resolved method, so deriving further (
Logo of Sprite) just needstoDrawable(logo)to pick upLogoVT. - Every emitted construct is ordinary Nim (refs, fields, procs, a
{.global.}vtable, a cast) — no{.emit}, no C, no pointer arithmetic, works on all backends.
The two earlier macros each implemented half of this: macro #1 got layer 3 but with a broken identity; macro #2 got a single-inheritance vtable. Combining layer 1+2 (composition + forwarding) for implementation with layer 3 (whole-object role interfaces) for polymorphism is, in my view, the cleanest MI you can emit in Nim.