Skipping parts of a for loop in the first iteration


#1

I crave a macro that does

@for x in iter begin 
    dosomethings()
    if $first_iteration
        y = dothings_differently(x)
    else
        y = dothings(y, x)
    end
    dootherthings()
end

branch free by rewriting into a preamble and a while loop.
It’s slightly above my head, any input welcome.

Edit: I should say, my hope is of course that someone else now wants this so much that they have to implement it.


Split iteration to head + tail
#2

Something like this?

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.)


#3

Thank you I will study this. Now I now why there is MacroTools. My failing attempt without it is already more than twice as much code.


#4

I can contribute now a nice quote body:

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.


#5

Alas, I carve this as well but lack the macro-fu to help. Please post here the solution and a MWE once you’re content. Thanks!!!


#6

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.


#8

Heureka!

"""
    @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

#9

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