Think of a proc like this:
proc foo(a,b,c,d: int) = ...
Is it possible to have a macro/template to call it like that:
discard foo(position(1,2),size(3,4))
Where position / size are just dummy names with no real meaning that "unpack" to the parameters. It would be ok that they need the types of the parameters of foo in their definition too.
It's not bound to tuples. something like
discard foo(bar(1,2,3),4)
should work too.
The bar(a,b,c) should just resolve to 1,2,3 such that foo is called as foo(1,2,3,4). Kinda like you would replace "bar(anything)" with "anything" before the compiler is parsing anything I guess.
I am not sure if that is possible at all (maybe only with a source code filter?).
Usage is mostly just for easier readability of the source-code and the target procs are imported C functions. They may have up to 12 parameters where some could be a bounding box for example which "combines" 4 of the parameters into "one" bounding box definition (like: rect(x,y,w,h) and a second pair for a coordinate inside that bounding box like "pos(x,y)" or a RGB value like "rgb(r,g,b)" but the C function expects just 3 bytes.
This could be something like this
foo( 0, 0, 100, 200, 50, 50, "test", 255, 0, 0)
and I want to be able to write
foo( rect(0,0,100,200), pos(50,50), "test", rgb(255,0,0) )
instead.
Shouldn't you wrap this low level C wrapper into a higher level version with custom types? Tuples and a few inline methods seem to match what you want in terms of syntax and will likely allow the end user of your library more flexibility in terms of keeping/passing around these intermediate objects rather than scary unpacking magic:
proc foo(a, b, c, d, e, f: int, g: string, h, i, j: int) =
echo "raw foo"
type
rect = tuple[x, y, w, h: int]
pos = tuple[x, y: int]
rgb = tuple[r, g, b: int]
proc foo(r: rect, p: pos, g: string, c: rgb) {.inline.} =
echo "intermediate foo"
foo(r.x, r.y, r.w, r.h, p.x, p.y, g, c.r, c.g, c.b)
proc makeRect(a, b, c, d: int): rect {.inline.} = (a, b, c, d)
proc test() =
foo(0, 0, 100, 200, 50, 50, "test", 255, 0, 0)
foo((0, 0, 100, 200), (50, 50), "test", (255, 0, 0))
foo((x: 0, y: 0, w: 100, h: 200), (50, 50), "test", (255, 0, 0))
foo(makeRect(0, 0, 100, 200), (50, 50), "test", (255, 0, 0))
when isMainModule: test()
Nice.
I wonder if Nim has something like the Ruby splash operator also? That is, in Ruby we can put a * in front of an array to get the array elements, so when we have
proc foo(a, b: int) = ... var a = [1, 2] discard foo(*a) is the same as discard foo(a[0], a[1])
Sure @gradha, that was my first thought actually.
It would mean we had to wrap a lot of procs (probably over > 100 of them). But thats not the end. You could want to hand the rect() and just two ints for the position. So you would need permutations of possible parameters. In practice this may be not the case though. One would force the user to use the custom types.
But I don't want that. It may be "wrong" in the end but currently I strife for a more simplistic solution. I would even accept inline comments as solution :-P
foo(/*rect*/ 0, 0, 100, 100, /*pos*/ 50, 50, /*rgb*/ 255, 0, 0)
BTW: We also use a converter for string->InternalString and InternalStringw->string that works perfectly (so far) without the need to write a wrapper for every external function.
I can just write foo("test") and that gets converted to InternalString (and is consumed by foo() which frees that memory itself later).
Or even vice versa let s:string = bar() where bar() returns the InternalString, we convert it to string and free the memory of the returned type at once.
While if you need the InternalSting as input for another proc, which expects the InternalString type we just use let s:InternalString = bar() (or no type at all). So foo(bar()) works and uses the InternalString without conversions.
But this then also works for echo normalize(bar()) or even foo(normalize(bar()) and I think this is pretty cool considering that foo/bar's InternalString is a total stranger to Nim.
As nearly every proc of the imported library consumes it's parameters, there is also no need to add wrapper objects to maintain memory allocations. I thought this is needed and started to do that. But it is not. Which was kinda surprise for me tbh.
Back to what I asked for:
I guess it is possible to create a macro which parses its parameters and rebuilds that into a call to a given proc name? Something like:
mixmacro(foo,mRect(1,2,3,4),5,6,mRGB(7,8,9))
that generates the following code/AST:
foo(1,2,3,4,5,6,7,8,9)
But this type of call looks ugly :(
Maybe a macro which creates a macro which can be called like that would be better?
genmixed(foo, mmFoo)
# here mmFoo() would be a macro generated from former macro
mmFoo(mRect(1,2,3,4),5,6,mRGB(7,8,9))
I would like to change most of the proc names of the imported library anyway so that is probably the way to go. Humm...
Is that a possibility and are there others?
import macros
proc internalBar(a, bb, c, d, x, y, r, g, b: int) =
echo locals()
proc internalBaz(a, bb, c, d, x, y, r, g, b: int) =
echo locals()
# we need these dummy constructors due to the wrong implementation
# of 'varargs[untyped]' in the compiler:
proc point(x, y: int): int = discard
proc color(r, g, b: int): int = discard
proc rect(a, b, c, d: int): int = discard
template declareUnpackingMacro(foo) =
macro foo(n: varargs[untyped]): untyped =
result = newNimNode(nnkCall, n)
result.add bindSym("internal" & astToStr(foo))
for x in n.children:
var unpack = false
if x.kind in nnkCallKinds:
case $x[0]
of "point":
expectLen(x, 3)
unpack = true
of "rect":
expectLen(x, 5)
unpack = true
of "color":
expectLen(x, 4)
unpack = true
else: discard
if unpack:
for i in 1..<x.len: result.add x[i]
else:
result.add x
declareUnpackingMacro(bar)
declareUnpackingMacro(baz)
bar(rect(1, 2, 3, 4), point(8, 9), color(7,7,8))
Hey @Araq! Thank you for your time! Thats pretty cool!
Sadly it does not work because it complains about different types in my real code.
See this:
import macros
# I made that "cint" instead of "int"
# real code uses converters there.
proc internalBar(a, bb, c, d, x, y, r, g, b: cint) =
echo locals()
# we need these dummy constructors due to the wrong implementation
# of 'varargs[untyped]' in the compiler:
proc point(x, y: int): int = discard
proc color(r, g, b: int): int = discard
proc rect(a, b, c, d: int): int = discard
# added this for testing
proc one(a: int): int = discard
template declareUnpackingMacro(foo) =
macro foo(n: varargs[untyped]): untyped =
result = newNimNode(nnkCall, n)
result.add bindSym("internal" & astToStr(foo))
for x in n.children:
var unpack = false
if x.kind in nnkCallKinds:
case $x[0]
of "one":
expectLen(x, 2)
unpack = true
of "point":
expectLen(x, 3)
unpack = true
of "rect":
expectLen(x, 5)
unpack = true
of "color":
expectLen(x, 4)
unpack = true
else: discard
if unpack:
for i in 1..<x.len: result.add x[i]
else:
result.add x
declareUnpackingMacro(bar)
bar(rect(1, 2, 3, 4), point(8, 9), color(7,7,8))
# bar(1,2,3,4,8,9,7,7,8) # gives error cint!=int
# following works
bar(one(1),one(2),one(3),one(4),one(8),one(9),one(7),one(7),one(8))
# this works too
bar(cast[cint](1),cast[cint](2),cast[cint](3),cast[cint](4),cast[cint](8),cast[cint](9),cast[cint](7),cast[cint](7),cast[cint](8))
@Araq: That does not change it. Especially as "this" line is working as expected. Which I found interestingly and probably part of the solution. This is why I made the "one()" wrapper for a single Argument which also worked.
It is the else part which does not work. When I do that bindSym() there it works for this example.
But that will not solve the problem I have because of the real code has mixed types in the parameters so I can not just bind them all as 'cint'.
Now I feel teased... it probably may really work but how?
It would need to work something like those:
proc internalBar(a, bb, c, d: cint, s: string, r, g, b: int, data: ptr) =
echo locals()
proc internalBaz(t, l, w, h: cint, title: string, px, py: cint, text: string) =
echo locals()
Ideas?
I got something which seems to serve my wished:
import macros
proc internalBar(top, left, width, height: cint, s: string, x, y: int, r,g,b: int) =
echo locals()
# we need these dummy constructors due to the wrong implementation
# of 'varargs[untyped]' in the compiler:
proc point(x, y: int): int = discard
proc color(r, g, b: int): int = discard
proc rect(a, b, c, d: int): int = discard
template declareUnpackingMacro(nimname,extname) =
macro nimname(n: varargs[untyped]): untyped =
var s: string = astToStr(extname) & "("
var first = true
for x in n.children:
var unpack = false
if x.kind in nnkCallKinds:
case $x[0]
of "point":
expectLen(x, 3)
unpack = true
of "rect":
expectLen(x, 5)
unpack = true
of "color":
expectLen(x, 4)
unpack = true
else: discard
if unpack:
for i in 1..<x.len:
if first:
first = false
else:
add(s, ", ")
add(s, repr(x[i]))
else:
if first:
first = false
else:
add(s, ", ")
add(s, repr(x))
add(s, ")")
echo s
result = parseStmt(s)
declareUnpackingMacro(bar,internalBar)
bar(rect(1, 2, 3, 4), "test", point(8, 9), color(7,7,8))
bar(1,2,3,4,"text",8,9,7,7,8)
let top: cint = 1
let left: cint = 2
let width: cint = 3
let height: cint = 4
bar(rect(top, left, width, height), "test", point(8, 9), color(7,7,8))
Output:
internalBar(1, 2, 3, 4, "test", 8, 9, 7, 7, 8)
internalBar(1, 2, 3, 4, "text", 8, 9, 7, 7, 8)
internalBar(top, left, width, height, "test", 8, 9, 7, 7, 8)
(left: 2, r: 7, x: 8, height: 4, s: test, width: 3, y: 9, top: 1, g: 7, b: 8)
(left: 2, r: 7, x: 8, height: 4, s: text, width: 3, y: 9, top: 1, g: 7, b: 8)
(left: 2, r: 7, x: 8, height: 4, s: test, width: 3, y: 9, top: 1, g: 7, b: 8)
It that legit?
import macros
# I made that "cint" instead of "int"
# real code uses converters there.
proc internalBar(a, bb, c, d, x, y, r, g, b: cint) =
echo locals()
# we need these dummy constructors due to the wrong implementation
# of 'varargs[untyped]' in the compiler:
proc point(x, y: int): int = discard
proc color(r, g, b: int): int = discard
proc rect(a, b, c, d: int): int = discard
# added this for testing
proc one(a: int): int = discard
template declareUnpackingMacro(foo) =
macro foo(n: varargs[untyped]): untyped =
result = newNimNode(nnkCall, n)
result.add bindSym("internal" & astToStr(foo))
for x in n.children:
var unpack = false
if x.kind in nnkCallKinds:
case $x[0]
of "one":
expectLen(x, 2)
unpack = true
of "point":
expectLen(x, 3)
unpack = true
of "rect":
expectLen(x, 5)
unpack = true
of "color":
expectLen(x, 4)
unpack = true
else: discard
if unpack:
for i in 1..<x.len: result.add x[i]
elif x.kind == nnkIntLit:
result.add newcall(bindSym"cint", x)
else:
result.add x
declareUnpackingMacro(bar)
bar(rect(1, 2, 3, 4), point(8, 9), color(7,7,8))
bar(1,2,3,4,8,9,7,7,8)
This is still a hacky solution. More appropriate would be to use rect and point types that you expand at callsite via type inspection (macro.getType for varargs[typed]) but this is also a more complex solution.
It that legit?
It's a gross hack but if it serves your needs ... ;-)
Here a sneak peak for what I wanted that:
# ListControl (some of the most important widgets for our case)
when false: # old code
let lcTable = wxListCtrl_Create(mainFrame, wxID_ANY, 0, 0, -1, 100, wxLC_REPORT)
wxSizer_AddWindow(sizer, lcTable, 0, wxEXPAND + wxAll, 10, nil)
discard wxListCtrl_InsertColumn(lcTable, -1, "Vorname", 0, 100)
discard wxListCtrl_InsertColumn(lcTable, -1, "Name", 0, 100)
discard wxListCtrl_InsertColumn(lcTable, 0, "Id", 0, 30) # insert 'in front'
else:
# new code using the unpacker Macro like this:
#
# wxcUnpacking(listCtrl, wxListCtrl_Create)
# wxcUnpacking(add, wxSizer_AddWindow)
# wxcUnpacking(insertColumn, wxListCtrl_InsertColumn)
let lcTable = mainFrame.listCtrl(wxID_ANY, mxPos(0, 0), -1, 100, wxLC_REPORT)
sizer.add(lcTable, 0, wxEXPAND or wxAll, 10, nil)
discard lcTable.insertColumn(-1, "Vorname", 0, 100)
discard lcTable.insertColumn(-1, "Name", 0, 100)
discard lcTable.insertColumn(0, "Id", 0, 30) # insert 'in front'
Not shure if "listCtrl()" or "createListCtrl()" will be used. But I think one gets what is going on.
Important to notice that the "old code" uses the "raw" imported wxc functions without any wrapper code. So for example the strings get converted to wxStrings "on the fly" while still having some (not all) stuff encapsulated in their types like wxLC_REPORT does not accept something out of the WxcLCStyles we define, whereas it merely is just an int64 originally.
P.S.: the only really related part is that mxPos(0, 0) in the first line of the new code :)