Crystal: Macro @type mistakenly uses module's type

Created on 25 Jun 2018  路  11Comments  路  Source: crystal-lang/crystal

I have the following scenario:
A class Foo is inside a module Top. There is a macro that I want to share amongst all classes in the Top module, but not outside of the module. So the macro is placed inside the module. For simplicity, here is some slightly modified code from the documentation.

https://play.crystal-lang.org/#/r/4e3n

module Top
  macro add_describe_methods
    def describe
      "Class is: " + {{ @type.stringify }}
    end

    def self.describe
      "Class is: " + {{ @type.stringify }}
    end
  end
end

module Top
  class Foo
    Top.add_describe_methods
  end
end

puts Top::Foo.new.describe # expected "Class is: Foo", actual is "Class is: Top"
puts Top::Foo.describe # expected "Class is: Foo", actual is "Class is: Top"

The first problem I encountered is how to reference the macro. Using just add_describe_methods gives an "undefined local variable or method" error. Top::add_describe_methods doesn't work either. The only working option (that I know of) is Top.add_describe_methods. But this gives the module name instead of the class name as expected. Perhaps this is a misunderstanding of how to share macros in a module.

$ crystal -v
Crystal 0.25.0 [7fb783f7a] (2018-06-11)

LLVM: 4.0.0
Default target: x86_64-unknown-linux-gnu

Most helpful comment

All 11 comments

You need to escape the {{ @type.stringify }} so evaluation is deferred to a later pass inside of Foo instead of Top: https://carc.in/#/r/4e4z

Thanks, that helps some. I have another example where this doesn't work (simplified my first code snippet a bit too much I guess). Escaping the expression causes a compilation error in this example. Not escaping it results in the Top being used. Any tips on this one?

https://carc.in/#/r/4e5h

module Top
  macro example
    def use_foo(foo : \{{ @type.id }})
      puts foo
    end
  end
end

module Top
  class Foo
    Top.example
  end
end

a = Top::Foo.new
Top::Foo.new.use_foo(a)
Error in line 11: macro didn't expand to a valid program, it expanded to:

================================================================================
--------------------------------------------------------------------------------
   1.     def use_foo(foo : {{ @type.id }})
   2.       puts foo
   3.     end
   4.   
--------------------------------------------------------------------------------
Syntax error in expanded macro: example:1: expecting token 'CONST', not '{{'

    def use_foo(foo : {{ @type.id }})
                      ^

================================================================================

and without escaping:

Error in line 16: no overload matches 'Top::Foo#use_foo' with type Top::Foo
Overloads are:
 - Top::Foo#use_foo(foo : Top)

I'm not sure on the rules in effect on that one, sorry. Maybe someone else can explain it.

I'm not sure how flexible your use case is, but typically I would accomplish what you're doing via an included hook instead using a mixin: https://carc.in/#/r/4e65.

https://carc.in/#/r/4e95

module FooFighters
  macro included
    def use_foo(foo : {{ @type.id }})
      pp foo
    end
  end
end

class Foo
  include FooFighters
end

Foo.new.use_foo(Foo.new)

Some code for thought, I don't know if that helps but thought you might want to take a look anyway.

That is, please explain what are you trying to achieve. Maybe you don't need {{ @type }} at all. And in the second snippet you have, you can just use self as a type restriction. Or no restriction at all...

It looks like I simplified my issue too much and you guys are finding solutions that work for those. All of these issues show the same end effect, that Top is used instead of the class name as I would expect. This result seems to be from internals that I don't fully understand. I would expect the class name to be used, but it seems that by invoking it as Top.add_describe_methods, the compiler is resolving @type to Top. And as mentioned in an earlier comment, that delaying the evaluation with \{{ @type.stringify }} fixes it, possibly by resolving the type to Foo at that point?

Ultimately what I'm trying to do is this:

module Top
  macro event(name, id_var, callback_setter_method, callback_types)
    alias {{ name.id.capitalize }}Callback = ({{ *callback_types }}) -> Nil
    @@%subscribers = Hash({{ @type.id }}, {{ name.id.capitalize }}Callback).new

    def on_{{ name.id }}(&block : {{ name.id.capitalize }}Callback)
      @@%subscribers[self] = block
      {{ callback_setter_method.id }}({{ id_var }}, ->{{ @type.id }}.{{ name.id }}_callback)
    end

    protected def self.{{ name.id }}_callback({% for t, i in callback_types %}_var{{ i }}{% if i < callback_types.size - 1 %}, {% end %}{% end %})
      caller     = new(_var0) # First variable is unique ID.
      subscriber = begin
        @@%subscribers[caller]
      rescue ex : KeyError
        raise Exception.new("Callback for non-existent subscriber - shouldn't be here!", ex)
      end
      subscriber.call({% for t, i in callback_types %}_var{{ i }}{% if i < callback_types.size - 1 %}, {% end %}{% end %})
    end
  end
end

This is why I simplified my original examples :wink:
What the code attempts to do is add an event handler that allows closures and passes it onto C-land. It is used like this:

module Top
  class Foo
    event :finish, @id, LibFoo.set_finish_callback, [Int32, Int32]
    def initialize(@id)
    end
  end
end

Top::Foo.new(1).on_finish do |id, result|
  puts "#{id} - #{result}"
end

The macro expands to:

alias FinishCallback = (Int32, Int32) -> Nil
@@__temp_24 = Hash(Top, FinishCallback).new

def on_finish(&block : FinishCallback)
  @@__temp_24[self] = block
  LibFoo.set_finish_callback(@id, ->Top.finish_callback)
end

protected def self.finish_callback(_var0, _var1)
  caller     = new(_var0) # First variable is unique ID.
  subscriber = begin
    @@__temp_24[caller]
  rescue ex : KeyError
    raise Exception.new("Callback for non-existent subscriber - shouldn't be here!", ex)
  end
  subscriber.call(_var0, _var1)
end

Notice that Top is used instead of Top::Foo for @type.

This macro code seems very complicated and messy for what I'm trying to achieve. Perhaps there's a better way of doing this. However, it's still strange to me that I get Top instead of Top::Foo when I use @type.

  • Invoking a method/macro will resolve to the immediate context. In your case, event happens inside Foo and it's searched there, not in the enclosing namespace (that's not how the language works)
  • @type inside a macro will expand immediately. @type inside a method expands when the method is invoked. That's called a macro method, and it allows expanding the method differently for each type.
  • The usual way to model this in Crystal is to define a module with the macro, and have the class include that module, which makes all the module's macros available to that class. Like that, if you use {{@type}} inside those macros, because you are already inside the class that invoking the macros, you'll get the correct behaviour.

Invoking a method/macro will resolve to the immediate context. In your case, event happens inside Foo and it's searched there, not in the enclosing namespace (that's not how the language works)

This is my misunderstanding then. I was under the impression that it worked like class resolution. For instance:

module Top
  class Foo
  end
  class Bar
    # Can reference Foo as just `Foo` here, not `Top::Foo`
  end
end

@type inside a macro will expand immediately. @type inside a method expands when the method is invoked.

This is what I suspected after getting feedback. Good to know!

The usual way to model this in Crystal is to define a module with the macro, and have the class include that module, which makes all the module's macros available to that class.

It seems strange to include a module that a class is nested in:

module Top
  class Foo
    include Top
  end
end

So to achieve this, I would rather do the following:

module TopUtil
  macro example
    # ...
  end
end
module Top
  class Foo
    include TopUtil
  end
end

But this does seem like the best solution to my problem.

Thanks for the clarification!

Well, Top isn't a good name either. The usual approach is to use a module as a namespace for your library, and then a module (and other stuff) inside that. In this case it seems you want some sort of callback, so maybe the module should be named Callbacks, or EventSource, or something like that (no idea what you are building)

I just used Top as an example. I use the name of the shard for the module name in my actual code. I like the name Callbacks for containing the macros and utilties around that.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lbguilherme picture lbguilherme  路  3Comments

RX14 picture RX14  路  3Comments

asterite picture asterite  路  3Comments

oprypin picture oprypin  路  3Comments

Sija picture Sija  路  3Comments