Hello,
at the moment i am learning generics and macros. So I had the idea of implementing some kind of easy callbacksystem, where the user must specify an 'Event[T: proc]', so that he can later call 'execute' on the event. The execute function then should have the Event as first param, followed by the params defined in the proc given to the event. Executing the event should look like i just call the procedure directly.
Will it be possible to create something like the following with the help of macros or can we have some mechanism in the language to inject and forward params in generics?
type
Event[T: proc] = object of TObject
handlers: seq[T]
proc execute[T] (e: Event[T], injectedParams: T.params) = # this will extend to (e: Event[T], p1, p2, p3: int)
for i in countdown(e.handlers.len - 1, 0):
h = e.handlers[i]
if h not nil:
h(injectedParams) # extends to h(p1, p2, p3)
type
MyEventHandler = proc (p1, p2, p3: int)
SomeObject = object of TObject
my_event = Event[MyEventHandler]
var a: SomeObject
a.my_event.execute(1, 2, 3) # Is this possible?
There is no similar mechanism at the moment, but let's discuss the possibilities. You could only do something similar to a C++ solution by defining a separate overload for each number of params, but obviously this is not as elegant as the proposal here.
There is a planned support for heterogeneous generic varargs[any] procs that will collect their params into a tuple that can then be expanded if necessary at call sites. Something like T.params could be easily defined as a magic from the typetraits module that returns a tuple type. Then we'll only need a special pragma telling the compiler that a tuple should be expanded in the proc signature and the rest of the code will be shared with what we need for the generic varargs tuples.
proc execute[T] (e: Event[T], injectedParams: T.params {.unpacked.})
Araq, your thoughts?
My current design looks like this: There are pack and unpack operations that deal with tuples. Well not really. ;-) In order to not complicate the type system with yet another magic type like arglist[expr], we will have call and namedCall operations. So instead of f(unpack(a)) we will have call(f, a). This way no changes to the type system are necessary and named calls are easily supported too: If tup == (x: 2, y: 3) then namedCall(f, tup) produces f(x=2, y=3).
So C++-styled "perfect forwarding" works like this:
type
MyThread[P: proc; A: tuple = P.params] = object
f: P
args: A
var
threads: seq[MyThread[proc(a,b:int)]]
proc runAll =
for t in threads: call(t.f, t.args)
proc register(f: proc, args: varargs[expr]) =
threads.add(MyThread[f.type](f=f, args=pack(args)))
proc foo(x,y: int) =
echo x, y
register(foo, 1, 2)
runAll()
So as a minimum we need a type trait params that extracts the parameters of a type T: proc as a tuple and pack and call.
Hello,
i guess if i write the following code, my example should work. But then, will it be evaluated at compiletime? And will the error point to the position where the wrong calling was made?
type
Event[T: proc] = object of TObject
handlers: seq[T]
proc execute[T] (e: Event[T], p: varargs[expr]) =
for i in countdown(e.handlers.len - 1, 0):
h = e.handlers[i]
if h not nil: call(h, pack(p)) # Is this compiletime checked? p matches h?
type
MyEventHandler = proc (p1, p2, p3: int)
SomeObject = object of TObject
my_event = Event[MyEventHandler]
var a: SomeObject
a.my_event.execute(1, 2, 3)
a.my_event.execute(1, 2, 3, "fail") # Will this error be detected at compiletime? And will the error point to this line?
The execute proc will be compiled as a generic for each unique set of parameter types, then it will correctly enforce the type safety in the call within the body.
The error you are going to get will be one-level deep generic error on the line of the call within the body, with "instantiated from here" pointing at the execute call. This error can be improved a little bit if something like the .unpacked. pragma is supported. Also, you wont be able overload execute with other procs as an additional minor limitation of the varargs approach.