using MacroTools: postwalk, @capture
macro metafor(loop)
firstexpr = nothing
loop = postwalk(loop) do x
if @capture(x, for idx_ ∈ iter_ inner_ end)
quote
for $idx ∈ $iter
$idx == first($iter) && continue
$inner
end
end
elseif @capture(x, @first(y_))
firstexpr = y
nothing
else
x
end
end
esc(quote
$firstexpr
$loop
end)
end
which gives
@metafor for i ∈ 1:5
@first println("first!")
println("not first")
end
>>>
first!
not first
not first
not first
not first
It’s a little messy and can certainly be improved on, but taht should give you a rough idea of a possible implementation. (Of course you could do endless variations based on which code you want to execute in which iteration.)
quote let ϕ = iterate($iter)
while ϕ != nothing
$i, st = ϕ
$(stms1...)
while true
ϕ = iterate($iter, st)
ϕ != nothing && break
$i, st = ϕ
$(stms...)
end
break
end
end end
Edit: Small fix, still something goes wrong if one uses continue in the first iteration.
Writing macros in Julia without the aid of MacroTools is indeed a nightmare. It really should have been in the stdlib. Once you mess around with MacroTools a bit, you’ll probably find it much easier. Just remember: you should think of a macro as a simple syntax transformation and you should always think about how you would write your code without a macro (i.e. the desired output of the macro) before you start writing the macro.
"""
@unroll1 for-loop
Unroll the first iteration of a `for`-loop.
Set `$first` to true in the first iteration.
Example:
@unroll1 for i in 1:10
if $first
a, state = iterate('A':'Z')
else
a, state = iterate('A':'Z', state)
end
println(i => a)
end
"""
macro unroll1(expr)
@assert expr isa Expr
@assert expr.head == :for
iterspec = expr.args[1]
@assert iterspec isa Expr
@assert iterspec.head == :(=)
i = iterspec.args[1]
iter = iterspec.args[2]
body = expr.args[2]
body_1 = eval(Expr(:let, :(first = true), Expr(:quote, body)))
body_i = eval(Expr(:let, :(first = false), Expr(:quote, body)))
quote
local st, $i
@goto enter
while true
@goto exit
@label enter
ϕ = iterate($iter)
ϕ === nothing && break
$i, st = ϕ
$(body_1)
@label exit
while true
ϕ = iterate($iter, st)
ϕ === nothing && break
$i, st = ϕ
$(body_i)
end
break
end
end
end
Cool macro! My only objection would be that readability is a bit reduced, since developers not familiar with that macro might not understand at first glance what the code is doing.
Another alternative:
for (i,c) in enumerate('A':'D')
if i == 1
print("first")
else
print("not first")
end
println(" body $c")
end
Results in:
first body A
not first body B
not first body C
not first body D
using IterTools: flagfirst
function fsum(f, xs, default)
local y
for (isfirst, x) in flagfirst(xs)
if isfirst
y = f(x)
else
y = y + f(x)
end
end
if @isdefined(y)
return y
else
return default
end
end