Conversation
|
Some personal observations:
|
I actually wanted to post an RFC like this soon, only adding type restrictions and the ability to call macros weithin macros, but leaving the macro def syntax as a future consideration. I think I'll now wait how this RFC progresses. |
|
@BlobCodes that's an intriguing perspective. macro def format_name(name : StringLiteral) : StringLiteral
name.underscore.upcase
end
macro format_name(name : StringLiteral) : StringLiteral
{{ name.underscore.upcase }}
end |
macro generate_constant(name)
CONST_{{ name.id }}
end
|
There is no explicit return value, the entire macro is executed as it works right now, its entire output is parsed into an ASTNode and this ASTNode is then returned to the caller. If the given example macro was called with the name "foo", it would return Path("CONST_foo").
Yes, explicit returns would probably conflict with the current template syntax. That's something that could be explored with macro defs.
Why should a macro result in undefined behaviour if it's called within a nested context? |
I was mainly thinking about my use case where some of my macro foo(x)
{%
# Interacting with constants and stuff
# Intended to be called within a macro expression at certain places in the code
%}
endSo if you tried to use this macro outside of a macro expression where you're expected for it to run, what would it do? Probably error that the const is unknown? Maybe not a big deal in practice, but this is kinda why i liked |
|
This pull request has been mentioned on Crystal Forum. There might be relevant details there: https://forum.crystal-lang.org/t/dependency-injection-with-crystal-macros/8684/2 |
I would be in favour of having one lookup path, otherwise I think you'd end up with a lot of template macros that just call a macro def and splat the result into the program, ie: macro def foo(args) : StringLiteral
"from macro method: #{args.stringify}"
end
macro foo(args)
{{ foo(args) }}
end
foo "hello!"Since both types of macros are "returning" a syntax tree (one with a template, the other with an actual return value) I would like them to both insert that syntax tree into the program when called. A possibility to consider is to only have macro defs and have a macro make_class(name)
puts "I'm making a class called #{name}"
ast = quote do
class {{ name }}
end
end
puts "I've made this class: #{ast}"
ast
endThat way the return value semantics are clearer. This would obviously be a breaking change so not really practical, but you could have a tool that converted code between the two. Either way I'd love macro methods in whatever form they come in. |
What would the use case/reasoning for this be? Because in this example you don't even need the macro foo(args)
{{ "from macro method: #{args.stringify}" }}
end
foo "hello!" |
|
I should've been clearer, I mean if I have a |
|
Ahh, yea I'm not too concerned about that. There isn't anything you can do in a |
|
@willhbr Your comment got me thinking whether it might be an option to differentiate between the two modes by return type: a macro that returns a value must have a return type (like |
|
I could go with that. Is a bit easier to explain too: a macro def is a macro with a return type, vs adding a whole new It does make this a bit more similar to one another, but still fairly obvious which is which. class Foo
macro foo
"from regular macro"
end
macro foo : StringLiteral
"from macro method"
end
endWhat should be done about macro parameters. Can a normal macro have typed parameters like a macro method too? |
|
If a normal/template macro doesn't have a return type, would you still be able to call it from a macro def? I don't know if this would actually end up being useful—it's hard to know without using it—but I think it would be useful to allow extracting parts of templates into separate methods to make code more readable/testable/etc. I thought through rewriting my dependency injector macro if I had macro methods, and I came to this section: def self.inject(scope)
{{ @type }}.new(
{% for arg in method.args %}
{% if arg.restriction.nil?
arg.raise "needs restriction on #{arg}"
end %}
{% if arg.restriction.resolve?.nil?
arg.raise "Unable to resolve #{arg.restriction}"
end %}
{% if arg.restriction.resolve < Geode::Injector::Provider %}
{{ arg.restriction }}.inject(scope),
{% elsif arg.restriction.resolve < Geode::Injector::Lazy %}
{{ arg.restriction }}.inject(scope),
{% else %}
scope.get({{ arg.restriction }}),
{% end %}
{% end %}
)
endI could pull out the verification into a separate macro def easily: macro def validate_injectable_arg(arg)
if arg.restriction.nil?
arg.raise "needs restriction on #{arg}"
end
if arg.restriction.resolve?.nil?
arg.raise "Unable to resolve #{arg.restriction}"
end
endBut I'm not sure if I could do the same for the code that generates the actual code in the loop. It would want to be a template macro, so I could do this: macro build_creation(arg, scope)
%scope = {{ scope }}
{% if arg.restriction.resolve < Geode::Injector::Provider %}
{{ arg.restriction }}.inject(%scope),
{% elsif arg.restriction.resolve < Geode::Injector::Lazy %}
{{ arg.restriction }}.inject(%scope),
{% else %}
%scope.get({{ arg.restriction }}),
{% end %}
endThen the code generated by the main macro would look like this: SomeInjectableType.new(
build_creation(some_arg : HTTP::Context),
build_creation(another_arg : User),
)Which I guess is fine, but it does mean we've got another layer of macro expansion that needs to happen, and it's a bit harder to see what the final generated code is ( It would be nice to be able to call any macro from any other macro and have the result inserted into the code, so I could do: def self.inject(scope)
{{ @type }}.new(
{% for arg in method.args %}
{% validate_injectable_arg arg %}
{{ build_creation(arg, MacroId.new(:scope)) }},
{% end %}
)
endWould I be able to create and return an actual AST from |
|
perfer Introduce this is for DRY when macro, eliminate duplicate code, but, I don't see any need for a block at this point—this is certainly not something that should be on the agenda for the current version. (Translated with qwen3.5:35b) |
I been thinking about this and that's a fair point. I'd have to see how this works in my POC branch. My current thinking is it would be the same if there is just a single ASTNode being returned, but having a macro able to inline the output of another macro feels like a different but related use case. You can of course expand a macro as part of the output of another macro so could maybe do something like: def self.inject(scope)
{{ @type }}.new(
{% for arg in method.args %}
{% validate_injectable_arg arg %}
build_creation({{ arg }}, :scope),
{% end %}
)
endWhere |
Preview: https://github.com/crystal-lang/rfcs/blob/1195640eaf4f6780bb02e1839a35552adb42d04e/text/0018-macro-methods.md
PoC PR: TODO
A proposal based on the OP from crystal-lang/crystal#8835. Simple re-usable macro methods without the extra AST node monkey patching stuff.