What about having a build-in marco method like that:
# Expands *code* into the real crystal code
macro_def expand(code : StringLiteral) : Nop
end
And the two rules like this:
Then, to have a build-in macro method capture(&)
, with the following behavior:
- It captures every output of
expand
and append it to a string
- then if something have been added to the string, returns the string parsed as ASTNode
- otherwise, returns the result of given block.
e.g:
{% capture do
expand("x = 42")
end %} # generates nothing, returns `x = 42` (Assign)
{% capture { 0 } %} # generates nothing, returns 0 (NumberLiteral)
{% capture { expand("") } %} # generates nothing, returns Nop
{% capture { expand("+=)") } %} # Error: captured expansion should be syntactically valid.
This way, each
and map
could be implemented like this:
macro_class ArrayLiteral
macro_def each(&)
i = 0
while i < self.size
yield self[i]
end
end
macro_def map(&)
arr = [] of ASTNode
each do |x|
arr << capture do # we capture expansion and parse it if any, else returns element inside the yield
yield x
end
end
arr
end
end
So then:
{% [1, 2, 3].each do |x| %}
puts {{x}}
{% end %}
# => transformed to:
{% [1, 2, 3].each do |x| %}
puts {% expand(x.to_s) %}
{% end %}
# => transformed to:
{% [1, 2, 3].each do |x|
expand("\n puts ")
expand(x.to_s)
expand("\n")
end %}
# generates: " puts 1\n puts 2\n puts 3\n", returns `Nop`
# it doesn't capture expansions, letting them expand to the code.
Then a map
would works intuitively:
{% nodes = [1, 2, 3].map do |x| %}
{% if x % 2 == 0 %}
puts {{x}}
{% else %}
puts {{-x}}
{% end %}
{% end %}
# =>
{% nodes = [1, 2, 3].map do |x|
expand("\n ")
if x % 2 == 0
expand("\n puts ")
expand(x.to_s)
expand("\n ")
else
expand("\n puts ")
expand(-x.to_s)
expand("\n ")
end
expand("\n")
end %}
# => generates nothing, returns [puts(-1), puts(2), puts(-3)] (ArrayLiteral)
# it capture expansions, using them.
And a classical map would works as usual, since capture
returns the result of the block if no expansion is performed:
{% x = [1, 2, 3].map(&.to_s) %} # returns ["1", "2", "3"] (ArrayLiteral)
Of course expand
, capture
and marcro defs donât need to realy exist, it could be hard-coded and hidden for the user.
select
, and all other classical methods could be implemented similarly.
This end up to be equivalent to what you stated @HertzDevid, with the key difference that we decide which methods capture or let expand.
But I found this could be a good way to solve at last the dilemma of for
vs each
.
Thinking?