How to capture a for loop index after break?

Is there a way to have a non-local scope loop index, so code similar to this:

for i = 1:9
  if i > 3
    break
  end
end

println(i)

works and I don’t have to write:

j = 0

for i = 1:9
  if i > 3
    j = i
    break
  end
end

println(j)

no easy / canonical way because for...end forms a local scope, there’s this hack:

julia> j = Ref(-1)
Base.RefValue{Int64}(-1)

julia> for j[] = 1:10
           if j[]>3
               break
           end
       end

julia> j[]
4

But I’d recommend against it.

Was for global i = 1:9 syntax ever proposed?

Are you looking for the outer keyword?

function foo(p)
   local i
   for outer i = 1:100
       rand() > p && break
   end
   return i
end
14 Likes

:open_mouth: , first time seeing this anywhere and ?outer has nothing to offer, neither do for’s manual page or docs. Btw you don’t need the first local I think.

4 Likes

Same here. Really hard to find.

It does seem hard to find. The documentation mentions this feature here:
https://docs.julialang.org/en/v1/manual/variables-and-scoping/#Loops-and-Comprehensions

2 Likes

True. This works fine:

function foo(p)
   i = 0
   for outer i = 1:100
       rand() > p && break
   end
   return i
end

You just need the variable to exist in the outer scope. Using local is the least constraining way of doing that.

For example if you decide later to change the type of the loop variable (e.g. to for i = 1.0 : 10.0), having i=0 hardcoded will lead to a type instability. With local, i will always have the right type.

Another advantage is that you can reliably check whether the loop ran at all:

julia> function testfun(p)
           local i
           for i = 1:100
               rand() > p && break
           end
           if @isdefined(i)
               return i
           else
               return 999
           end
       end
testfun (generic function with 1 method)

julia> testfun(0.99)
999
3 Likes

In this case it would be easier to just return I from the loop?

You mean return i, right? My actual use case is more complex and I can’t do it, but in the foo example above, it is definitely an option.

In this case I would rather write:

function testfun(p)
  i = 999
  for outer i = 1:100
    rand() > p && break
  end
  return i
end

but I see where local may be useful.

BTW, outer is not recognized as a keyword by Discourse.

That’s because it’s not actually a reserved keyword, outer only has special meaning in the iteration specification of for loops and generators. That’s why the following are still completely valid syntax:

julia> outer = 7
7

julia> for outer in 1:3
           println(outer)
       end
1
2
3

julia> for outer * inner ∈ 1:3
           println(1 * 2)
       end
1
2
3
4 Likes

Hmm, that looks quite a bit error-prone.

Why is that error prone? It would be pretty annoying not being able to name variables outer in normal code anymore, just for this special for loop syntax.

(I had no idea what for outer * inner ∈ 1:3 does and I had to stare at Meta.@lower output for a while. This is so diabolical and I love this example :rofl:)

8 Likes

OK, maybe not error-prone, since compiler will help here, but context sensitive behavior of outer may be a bit confusing. I don’t think identifiers, which may or may not be restricted keywords depending on context, are widely used in programming languages. Maybe I’m too old date…

Way too diabolical for me! Can you explain what is happening there? Why are we getting anything other than 2 as in:

julia>   println(1 * 2)
2

I don’t want spoil anyone’s fun so I’m hiding the explanation behind [spoiler] (click to remove the blur):

for outer * inner ∈ 1:3
    println(1 * 2)
end

behaves as above since it is equivalent to

for x ∈ 1:3
    _ * _ = x
    println(:this_is_ignored * :this_is_also_ignored)
end

(Note also that _ * _ = x is equivalent to *(_, _) = x; i.e., it defines a closure named *.)

Perhaps the best way to see what is going on is to print what * is:

julia> for outer * inner ∈ 1:3
           @show *
           println(:ignored * :ignored)
       end
* = var"#*#12"{Int64}(1)
1
* = var"#*#12"{Int64}(2)
2
* = var"#*#12"{Int64}(3)
3
8 Likes