Yet another OOP macro ;). [*]
I was wondering if it would be possible to write classes in Nim entirely by using closures. A good opportunity to work on my meta programming skills. At first the approach looked a bit weird, but now I feel that the resulting DSL might actually work quite well. What do you think?
[*] Actually I was searching for OOP macros on Nimble but couldn't find much...
So far I only ran some stupid benchmarks on dispatch speed here. and in that terms using closures is a bit faster than the branch based dispatch of methods, even if the number of subclasses is still low (but nothing substantial).
Obviously using closures is the wrong tool if a problem requires raw field access in tight loops (e.g. vector math library). The design rather aims at use cases that are well suited for OOP abstraction, for instance a React-like web components library, where field access is negligible.
Do you have a good idea of a benchmark that neither focuses too much on field access nor on the O(1) vs O(N) dispatch difference?
Nice work, this closure-as-an-object pattern is quite useful in JavaScript as an alternative to its IMHO convoluted prototype-based OO.
That being said, a syntax which retains the meaning of * and is more compact might be preferable, something more like Scala's classes perhaps:
# The ``*`` means what it always means in Nim: "visible outside of the
# defining module". The arguments of the default constructor are defined in
# the class declaration itself.
class Counter*(start {.private.}: int, step: var int) of SomeBaseClass:
# ``start`` is private, so it is just a constructor argument.
# ``step`` automatically also becomes a public value symbol (see below).
# The class definition body is the default constructor body.
# Public value symbols also become class properties: vars automatically get
# getters and setters by default, lets only getters.
# var step*: int = step <-- automatically inserted for public ctor arg.
let firstVal = start
var value* {.ro.}: int = start # read-only, no setter generated.
private:
var someSecret = 0 # No getter or setter generated for this.
proc change(op: proc(x, y: int): int) = value = op(value, step)
# Public message API.
proc inc*() = change(`+`)
proc dec*() = change(`-`)
# An alternative constructor.
ctor() = ctor(0, 1)
# The resulting public API in "concept notation" with export markers,
# c is an instance:
#
# Conter*(int, int) is Counter
# Counter() is Counter
# c.step*() is int
# c.`step=`*(int)
# c.firstVal() is int
# c.value*() is int
# c.`value=`*(int)
# c.inc*()
# c.dec*()
The public/private semantics follow Nim's public-by-default object field access rule, but it could easily be inverted to private-by-default. Nim's parser can process this, so it could be mapped to the functionality you already implemented.
@gemath That's also something I had in mind ;).
The problem with autogenerating getters/setters for var x* expressions is that closures don't allow the regular field / field= syntax as far as I can see. For instance:
type
Obj = ref object
step: proc(): int
`step=`: proc(x: int)
let o = Obj(step: proc(): int = 0, `step=`: proc(x: int) = echo x)
echo o.step()
o.`step=`(1) # I guess only this syntax is possible
So you cannot just use something nice like o.step = 1. Thus, it's probably better to use different names for the getter and setter. The auto-generated getter/setter could follow a fixed convention like step/setStep or getStep/setStep. In the end I preferred to give the user control over how they call them with the current getter/setter shortcuts.
I also tried to use the Counter of SomeBaseClass syntax, but for the overload analysis it is important to pass in the base class as a typed macro argument. Counter of SomeBaseClass would require an untyped argument, and I don't think it would be possible to access the type impl in this case. That's why I went for the slightly less appealing classOf(Sub, Base) syntax, and that's also why generic subclasses will probably require a minor repetition classOfGeneric(Sub[X, Y], Base, Base[X]) -- the first appearance of Base is a typed arg, the one with the generics is untyped.
for property access you can use a distinct type and for assignment an operator like :=
type
Obj = ref object
fStep: int
ObjStepProperty = distinct Obj
template step(self: Obj): ObjStepProperty = self.ObjStepProperty
proc `:=` (self: ObjStepProperty, val: int) {.inline.} =
self.Obj.fStep = val
converter toInt (self: ObjStepProperty): int {.inline.} = self.Obj.fStep
when isMainModule:
var o = Obj()
echo o.step
o.step := 2
echo o.step
Another way around this is overloading dot operators. Apparently this now works without the {.experimental: "dotOperators".} pragma:
type
Obj = ref object
getStep: proc(): int
setStep: proc(x: int)
template `.`(o: Obj, f: untyped): untyped = (o.`get f`())
template `.=`(o: Obj, f, v: untyped): untyped = (o.`set f`(v))
let o = Obj(getStep: proc(): int = 0, setStep: proc(x: int) = echo x)
echo o.step
o.step = 1
Counter of SomeBaseClass would require an untyped argument, and I don't think it would be possible to access the type impl in this case.
Would this help? It generates a call to a macro which takes the base class type as a typed arg:
import macros
type
B = object
field: int
macro typeInfo(b: typed): untyped =
echo b.getImpl.treeRepr
macro class(args: varargs[untyped]): untyped =
template tpl(b) = typeInfo(b)
result = getAst(tpl(args[0][2]))
class A of B:
discard
I didn't know that this can be solved by forwarding from one macro to another, will try that, looks really nice.
I will experiment a bit more regarding the field accessors when I'm back from vacation. Overall this has a bit lower priority to me, because the use cases I have barely require plain getter/setter. If a situation requires exposing raw data, plain objects are a perfectly fine solution already. I'd use this closure based approach exactly when I want to hide raw data behind an abstract interface.