Readability is definitely a subjective subject. While there isn’t a lot that can really be done to address that directly. There are some things that could be done to treat the underlying reason(s) of why is macro code usually so hard to read? As someone who works with them a lot, the main reason is that you can’t reuse your macro code. If you want to do some complex piece logic twice, you have to duplicate everything. IMO I still think [RFC] Macro Defs - Methods in Macro land · Issue #8835 · crystal-lang/crystal · GitHub would go a long way.
Some other issues that could go a long way in promoting a better user experience, and/or architectural patterns.
Make custom compile time errors in macros report the correct node:
- Macro raise doesn't keep location · Issue #7147 · crystal-lang/crystal · GitHub
- Keep type location when raising inside macro hooks · Issue #7394 · crystal-lang/crystal · GitHub
Plus some some stuff related to annotations (is better than the mutable constant approach most libs used in the past):
- [RFC] Annotations 2.0 · Issue #9802 · crystal-lang/crystal · GitHub
- Specify annotation targets · Issue #8148 · crystal-lang/crystal · GitHub
- Expose a base `Annotation` type · Issue #12655 · crystal-lang/crystal · GitHub
EDIT: Another pattern I realized you can do that helped a lot is if your macro does a bunch of logic before emitting any code, you can include all of that in a single {% ... %}
. E.g. go from:
{% paths = {} of Nil => Nil %}
{% defaults = globals[:defaults] %}
{% for a in m.args.reject &.default_value.is_a? Nop %}
{% defaults[a.name.stringify] = a.default_value %}
{% end %}
{% if (value = route_def[:defaults]) != nil %}
{% for k, v in value %}
{% defaults[k] = v %}
{% end %}
{% end %}
to
{%
paths = {} of Nil => Nil
defaults = {} of Nil => Nil
m
.args
.reject(&.default_value.is_a?(Nop))
.each { |a| defaults[a.name.stringify] = a.default_value }
if ann_defaults = route_def[:defaults]
ann_defaults.each { |k, v| defaults[k] = v }
end
%}
And you end up with much more normal Crystal looking code. Mainly since you can use normal #each
methods and dont need all the {% ... %}
everywhere.
ref: Routing component integration by Blacksmoke16 · Pull Request #141 · athena-framework/athena · GitHub