For a macro which implements C++-style object-orientation, it would be nice to be able to have implicit procedure arguments.
That is, instead of
(.. some class metod ..)
self.cromulate(42)
You'd like to be able to just write
(.. some class metod ..)
cromulate(42)
This feature would also be useful for DSLs in general. I'm working on a Gtk2-DSL, and I'd like to be able to do something like this:
button_new("My button"):
set_relief(gtk2.RELIEF_NONE)
Here's my suggestion:
How about a feature that allows us to set any number of implicit procedure arguments to all calls within a block? Maybe even a kind of implicit argument stack.
So, roughly speaking:
# Your DSL macro generates this:
block: # Or a stmt-list in general
pushArguments(self)
# Method body:
cromulate(42)
# End method body
popArguments() # This could perhaps be added implicitly by compiler, at end of stmt-list
The compiler would then, when trying to find a matching procedure, try to add those arguments to the beginning of the procedure call. It would try first without the implicit arguments, and then with, so that the call with implicit arguments have lower priority.
Is this possible? And what do you think about adding such a feature?
Like using? http://nim-lang.org/docs/manual.html#statements-and-expressions-using-statement. But, as the manual says:
Warning: The using statement is highly experimental and has to be explicitly enabled with the experimental pragma or command line option!
proc cromulate(self: T, other: T = default)
Arrrrrrrrr: Thanks! It's not always easy to keep track of Nims features these days. It doesn't seem to support multiple arguments. I think you could make the argument that it can be a useful generalization (and why not generalize?) But it should do for my purposes.
vbtt: There are many cases where implicit arguments can be very useful, and cut down a lot of boilerplate. Especially for DSLs. I also think Nim could attract a lot of game programmers who will want some kind of C++ style object orientation macros. I don't feel a need for it myself, but I figured that example was more relatable to more people.
I'm not sure I understand your ambiguity example, could you elaborate?
As for DSLs, consider:
entry_new():
widget.set_max_length(10)
widget.set_has_frame(false)
widget.set_alignment(1.0)
widget.set_size_request(100, 200)
VS
entry_new():
set_max_length(10)
set_has_frame(false)
set_alignment(1.0)
set_size_request(100, 200)
If you feel adventurous you can take the using statement and mix it with an existing OOP macro so that you don't have to write this inside your method bodies. I don't think the multiple parameters idea is worth following because since it requires you to write the parameters before the block it doesn't reduce you much typing, and in any case it is likely you can write two separate blocks which makes the code cleaner.
With regards to your DSL proposal if you squint enough it is essentially the builder pattern so you would in theory write the following code in Nim:
widget.init
.set_has_frame
.set_size_request(666, 42)
.set_max_length(912)
Of course you can't because the compiler will complain with Error: expression 'init(widget)' has no type (or is ambiguous) or something similar because none of those calls are returning the object you want to chain on. With a macro we can fix this and unwrap recursively all the dot expressions into a flat list of calls so it works with existing Nim style APIs unaware of this pattern:
import macros
type
Something = object
max_len: int
has_framerate: bool
size_request: tuple[x, y: int]
proc init(s: var Something ) =
s.max_len = 255
s.has_framerate = false
s.size_request = (-1, -1)
proc init_something(): Something =
result.init
proc set_max_length(s: var Something, max_len: int) =
s.max_len = max_len
proc set_has_frame(s: var Something) =
s.has_framerate = true
proc set_has_frame(s: var Something, has_framerate: bool) =
s.has_framerate = has_framerate
proc set_size_request(s: var Something, x, y: int) =
s.size_request.x = x
s.size_request.y = y
proc has_dot_or_call(n: Nim_node): bool {.compile_time.} =
## Returns true if n has a first child which is a call or dot expression.
if n.is_nil: return
if not (n.kind in {nnk_dot_expr, nnk_call}): return
assert n.len > 0
case n[0].kind
of nnk_dot_expr, nnk_call:
result = true
else:
discard
proc unwrap_calls(n: Nim_node, var_name, builder: var Nim_node,
ancestor: Nim_node_kind = nnk_none):
Nim_node {.compile_time, discardable.} =
## Unwraps nested call/dot expressions into a flat list of normal calls.
assert(not builder.is_nil)
assert(not n.is_nil)
assert var_name.is_nil
n.expect_kind({nnk_dot_expr, nnk_call})
n.expect_min_len(1)
if not n.has_dot_or_call:
# Base case, we have reached an ident node, store the name and return.
n[0].expect_kind(nnk_ident)
var_name = n[0]
else:
unwrap_calls(n[0], var_name, builder, n.kind)
if n.kind == nnk_call:
# Add the call node inserting a nested dot expression
n[0].expect_kind(nnk_dot_expr)
n[0].expect_len(2)
var
call_node = new_nim_node(nnk_call)
dot_expr = new_nim_node(nnk_dot_expr)
# We always pick the previously found name as first param.
dot_expr.add(var_name)
dot_expr.add(n[0][1])
call_node.add(dot_expr)
# Now add the rest of possible parameters.
for child in 1..<n.len:
call_node.add(n[child])
builder.add(call_node)
else:
# We are in a dot expression, should we insert ourselves a call above?
if ancestor == nnk_call:
# Let ourselves be processed in the parent node.
return
# Insert an empty parameter call with our own expression.
var
call_node = new_nim_node(nnk_call)
dot_expr = new_nim_node(nnk_dot_expr)
n.expect_len(2)
if n[0].kind != nnk_ident:
dot_expr.add(var_name)
else:
dot_expr.add(n[0])
dot_expr.add(n[1])
call_node.add(dot_expr)
builder.add(call_node)
macro build_new(body: expr): expr =
## Small wrapper around unwrap_calls to allow multiple statements.
result = new_stmt_list()
for root_node in body.children:
if root_node.has_dot_or_call:
var var_name: Nim_node
unwrap_calls(root_node, var_name, result)
else:
result.add(root_node)
proc test() =
var widget = init_something()
widget.set_max_length(10)
widget.set_has_frame(false)
widget.set_size_request(100, 200)
echo "Normal code ", widget.repr
widget.init
echo "After reinitialization ", widget.repr
build_new: widget
.init
.set_has_frame
.set_size_request(666, 42)
.set_max_length(912)
echo "After build macro ", widget.repr
build_new:
widget.init
echo "Running inside macro as blockā¦ ", widget.repr
widget.set_has_frame(false).set_size_request(2, 3).set_max_length(8)
echo "How did that end? ", widget.repr
when isMainModule: test()
Note how the macro allows you to write multiple separate statements inside, since all of them are expanded with the first dot expression parameter. If so you may want to rename the build_new macro to something more generic like chain_calls or just chain.rku makes a good point.
skyflex, to elaborate the ambiguity, consider what happens when I call cromulate(t) - does it call cromulate(self, t) or cromulate(t, default).
I don't see the problem, obviously cromulate(self, t) is called within a using environment. This is not much different from C++:
http://stackoverflow.com/questions/25862633/c-why-member-function-has-priority-over-global-function
In my original suggestion I suggested that cromulate(t) have priority over cromulate(self, t), the thinking being that you can always add the "self" yourself to make sure it calls the second one. But how would you call the first one if the second one has priority like using does now? Would module.cromulate(t) work?
I still don't see any ambiguity problems whichever way it's done. You prioritize either the implicit or the explicit form.
rku: I agree, it could cause confusion. Actually, you can get confusion both ways as the questions Araq linked to illustrate. You can always get confusion when you have implicit stuff happening. But often times it's a trade-off we want to make.
Anyway, I don't suggest changing priority for using. But I do still think it could be a good idea to generalize it (support more than one implicit argument).
Would module.cromulate(t) work?
Yes.