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
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?
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.
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.
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.{{@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.
Most helpful comment
https://en.m.wikipedia.org/wiki/XY_problem