I would like to create a new variable in a macro. My code can be reduce to:
macro test()
%p = "a = 3"
%p
end
test()
puts(a)
but i then get an error undefined local variable or method 'a'
I tried to wrap the %p
in {{ }}
or {% %}
but it does not compile ("unexpected token: %
")
EDIT
Here is some context.
I often have to use a two named tuples with some common fields that are then converted to JSON and sent to different clients.
a = {x: xx, y: yy, w: ww}
b = {x: xx, y: yy, z: zz}
Currently I writing both of them with a repetition. Values for ww,xx,yy,zz are not known at compile-time. I would like to replace that with a call to a macro doing the merge at compile-time.
I came up with the following code that is creating the line of code I currently input manually:
macro merge(common, aOnly, bOnly)
# Start, middle and end parts of target instructions
%p1a = "a = {"
%p1b = "b = {"
%p2 = ""
%p2a = ""
%p2b = ""
%p3 = "}"
# Creating the middle part
{% for key, value in common %}
%p2 = %p2 + "{{key.id}}" + ": " + "{{value}}" + ','
{% end %}
%p2a = %p2
{% for key, value in aOnly %}
%p2a = %p2a + "{{key.id}}" + ": " + "{{value}}" + ','
{% end %}
%p2b = %p2
{% for key, value in bOnly %}
%p2b = %p2b + "{{key.id}}" + ": " + "{{value}}" + ','
{% end %}
# Removing unneeded comma
%p2a = %p2a.chomp(",")
%p2b = %p2b.chomp(",")
# Display
puts(%p1a + %p2a + %p3)
puts(%p1b + %p2b + %p3)
# Definition of new variables
%p1a + %p2a + %p3
%p1b + %p2b + %p3
end
merge({x: xx, y: yy}, {w: ww}, {z: zz})
The main problem here is misunderstanding of %names
. This is not a construct that makes instructions compile time. Writing %a = 5
is the same as writing guaranteed_unique_name = 5
– it's a way to avoid interfering with other variables that may exist in the same scope, but it's still a normal runtime variable assignment.
Constructing strings and then outputting them at compile time is not a viable approach to using macros, and it almost always ends up being impossible, because you can't reassign a different value to a macro variable. But here's an example of what it might look like if it actually worked, still trying to use your approach with strings:
macro merge(common, aOnly, bOnly)
# Start, middle and end parts of target instructions
{%
p1a = "a = {"
p1b = "b = {"
p2 = ""
p2a = ""
p2b = ""
p3 = "}"
%}
# Creating the middle part
{% for key, value in common %}
{% p2 = p2 + "#{key.id}: #{value}," %}
{% end %}
{% p2a = p2 %}
{% for key, value in aOnly %}
{% p2a += "#{key.id}: #{value}," %}
{% end %}
{% p2b = p2 %}
{% for key, value in bOnly %}
{% p2b += "#{key.id}: #{value}," %}
{% end %}
# Removing unneeded comma
{% p2a = p2a.chomp(",") %}
{% p2b = p2b.chomp(",") %}
# Definition of new variables
{{(p1a + p2a + p3).id}}
{{(p1b + p2b + p3).id}}
end
merge({x: xx, y: yy}, {w: ww}, {z: zz})
Now I will just rewrite your example with more usual constructs:
macro merge(common, a_only, b_only)
a = {
{% for key, value in common %}
{{key}}: {{value}},
{% end %}
{% for key, value in a_only %}
{{key}}: {{value}},
{% end %}
}
b = {
{% for key, value in common %}
{{key}}: {{value}},
{% end %}
{% for key, value in b_only %}
{{key}}: {{value}},
{% end %}
}
end
merge({x: "a", y: 5}, {w: "b"}, {z: 7})
pp a, b
As you can see, the normal code you put in a macro's body is runtime code while things inside {% %}
and {{ }}
are executed at compile time. You can make compile time loops and intertwine them with normal code lines to create complex code naturally.
Hopefully that answers the question title "Assigning a value to a new variable in a Crystal macro": to assign something to a runtime variable you do the same thing as for outputting any runtime code in a macro – you just write it out, like a = 5
, because by default macros output runtime code, and only {%
and {{
delimit compile time constructs.
To reply to a comment, here is perhaps an even more idiomatic version, and it even illustrates the temporary/unique variable names. It uses them to create tuples and then the result of the macro's execution is a tuple of those which can be conveniently unpacked, instead of relying on side effects, which is rarely done.
macro merge(common, a_only, b_only)
%a = {
{% for key, value in common %}
{{key}}: {{value}},
{% end %}
{% for key, value in a_only %}
{{key}}: {{value}},
{% end %}
}
%b = {
{% for key, value in common %}
{{key}}: {{value}},
{% end %}
{% for key, value in b_only %}
{{key}}: {{value}},
{% end %}
}
{ %a, %b }
end
first, second = merge({x: "a", y: 5}, {w: "b"}, {z: 7})
puts first