The example below is not properly making use of macro defs, yet the inherited method seems to work like a macro method on line 20. It does not work the same way when called on line 26
Slightly simplified:
class Filter
def whoami
{% begin %}
{{ @type }}
{% end %}
end
end
class Thing1 < Filter
def whoami
"I'm overridden"
end
end
class Thing2 < Filter
end
thing1 = Thing1.new
thing2 = Thing2.new
puts thing1.whoami
puts thing2.whoami
instances = [thing1, thing2]
instances.each do |instance|
puts "#{instance} (#{typeof(instance)}) :: #{instance.whoami}"
end
https://play.crystal-lang.org/#/r/zls
The issue is that the outcome nor behavior of a method should change depending on what the compile time type of a variable its called on is (and ideally only the runtime type should matter). The easy fix is to make @type always refer to the type the current method is defined in, however having it refer to the runtime type the method is called upon seems to be a bit more intuitive.
Perhaps macro methods should move from an explicit language construct to an implicit one, a method that calls into the macro language becomes a macro method.
This is not a bug, it's how the compiler and language work. In the case of thing2.whoami the compiler knows thing2 is a Thing2, and when you invoke a method on it the compiler instantiates a method for that specific type. Yes, this might result in some code duplication at the end (in the generated code), but after optimizations and inlining this duplication is usually gone.
I don't understand what's the problem though. Did you forget to say its a macro def and you expected to notice the mistake sooner, or do you need this behaviour?
My idea was to automatically make all methods that have {{@type}} inside them become macro defs automatically. I think this is what you'd always want. See #2565 . Because if you don't want the {{@type}} to change, you can just put Foo instead.
@asterite I totally forgot to say its a macro def and did expect to notice the mistake sooner.
From my view whoami is being called in both cases on an instance of Thing2 but yielding different results, which seems like a bug to me. If I understand what you've written it's because in one case the compiler knows the variable is a instance of Thing2 and in the other it only knows that the the variable is an instance of a Filter, so the result makes perfect sense.
You idea to automatically make all methods that have {{@type}} inside them become macro defs automatically seems to make sense on some level, though personally I'm a little concerned about implicitly changing behaviour based on the presence of a keyword.
How do you feel about having the compiler print a warning instead?
We'd have to first ask the question: when would you want to use {{@type}} but not have the method be a macro def. My thought is: if I use {{@type}} is because I want it to change according to the class having it. Otherwise I would use Filter.
Stepping back a bit, imagine there's no concept of macro defs in the language, and {{@type}} always resolves to the most specific type in a method. That the compiler internally marks the method as "this needs to be instantiated for each specific type" is an implementation detail. That's the behavior I want to define, just that @type always resolve to the most specific type.
This way the language becomes more intuitive too. You just define methods, and the compiler takes care of solving @type.
@asterite Ah, okay that totally makes sense. Thanks for the explaination
I just pushed a fix for this. With this change your code prints:
I'm overridden
Thing2
#<Thing1:0x10b3f3f90> :: I'm overridden
#<Thing2:0x10b3f3f80> :: Thing2
which I guess is more expected.
As a side node, in this case just {{@type}} inside the method is good, there's no need for {% begin %} ... {% end %} here.
Most helpful comment
We'd have to first ask the question: when would you want to use
{{@type}}but not have the method be a macro def. My thought is: if I use{{@type}}is because I want it to change according to the class having it. Otherwise I would use Filter.Stepping back a bit, imagine there's no concept of macro defs in the language, and
{{@type}}always resolves to the most specific type in a method. That the compiler internally marks the method as "this needs to be instantiated for each specific type" is an implementation detail. That's the behavior I want to define, just that@typealways resolve to the most specific type.This way the language becomes more intuitive too. You just define methods, and the compiler takes care of solving
@type.