Search code examples
macroscrystal-lang

Assigning a value to a new variable in a Crystal macro


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})

Solution

  • 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