Confusion about scope in try-catch blocks

Regarding scoping rules this used to trip me up a bit:

Python:

>>> try:
...     x = 2
... except:
...     pass
... 
>>> x
2

Julia:

julia> try
           x = 2
       catch
       end
2

julia> x
ERROR: UndefVarError: `x` not defined
5 Likes

OK … so try catch are blocks of code with own scoping here, right? To make it work like in Python you probably need somehow declare the x as global to be available outside of the code block, right?

1 Like

Right, you can do local x outside the try, but it’s often simpler to just put the x outside in the first place.

julia> x = try
               1
           catch
               2
           end
1

julia> x
1

Julia can do this because try is an expression, not (as in Python) a statement, so it has a value and you can assign it to a variable.

8 Likes

Keeping in mind that REPL global scope is different, this does work in a function

function foo()
  try
    x=2
  catch
  end
end

foo()  #properly returns 2

Every now and then I have to remember that there are some things that I can’t test easily in the REPL without a function (like the try/catch example).

2 Likes

The equivalent to the REPL example would be as follows.

julia> function foo()
         try
           x=2
         catch
         end
         return x
       end
foo (generic function with 1 method)

julia> foo()
ERROR: UndefVarError: `x` not defined

There are two fixes that come to mind.

julia> function foo()
         x = 0
         try
           x=2
         catch
         end
         return x
       end
foo (generic function with 1 method)

julia> foo()
2

julia> function foo()
         x = try
           2
         catch
         end
         return x
       end
foo (generic function with 1 method)

julia> foo()
2
5 Likes

I was playing around with it a bit too, and here’s one I don’t quite get.

function bar()
    try
        x=2
    catch
    end
    x += 1
    return x
ends

bar()   # returns 3

Somehow the += operator is able to grab the value of x when return can’t, then assign to x in a way that can be returned.

2 Likes

Huh, that is weird. Could that be a bug? This fails (as I would expect it to):

julia> function bar()
           try
               x = 2
           catch
           end
           y = x + 1
           y
       end
bar (generic function with 1 method)

julia> bar()
ERROR: UndefVarError: `x` not defined

Maybe your example is spooky-action-at-a-distance? Julia looks for a variable binding anywhere in the enclosing scope, even if it is after the try-catch scope?

2 Likes

x += 1 is x = x + 1. By assigning to x in the outer scope (anywhere, either syntactically before or after the try scope), you’re establishing x as a local name avaliable in bar() — and then the x in the try loop is the same x.

Honestly, I think Julia’s scoping behaviors are quite prone to establish these sorts of “bad” mental models (akin to what I was saying above) because they mostly “do what you mean” so very often.

11 Likes

You need to make it bold - best the “anywhere” a font size larger … and or also after a font larger … to make the point apparent at the first glance. The issue with the mental model is that it usually thinks in terms like from left to right, from top to bottom … so outer scope expression seen AFTER the inner scope is intuitively not considered to be the outer scope for the inner one … By the way: the core of the idea of oOo is to work with the inner mental model - your awareness of it is surprising because it seems that there are as good as no programmer around aware of this … maybe you are “The ONE” able to grasp the idea a join me in my efforts of making the oOo approach to programming reality? I am myself not that smart and progressing very slowly … having hard time to explain what I intend as final outcome and what it should be good for if I am asking a question.

This is really hard to grasp … I suggest to find a way to explain it in a way which is better understandable pointing out the actual issue with the mental model which is the intuitive assumption that code lines AFTER the lines before are not able to change the meaning of the lines before. This is with scopes apparently not the case. The assignment AFTER the try/catch/end block changes the scope of the x used in the block - do I understand it right?

1 Like

The set of local names available isn’t imperatively defined/updated by a sequence of events that are executed, but rather it’s a static property of the given scope. Each scope has a concrete set of local names it can use. Those names are established by the assignments that occur in that scope (or explicit local declarations). And then inner scopes can inherit those names.

4 Likes

Scoping rules are probably the thing about Julia I dislike most. In Python you can summarize the rules with “only functions introduce a scope”, which is a simple rule you could get tattooed.
In Julia, the rule is more like “Most blocks introduce a scope, but not if a variable is used already local to a surrounding scope except if that is the top-level of the REPL. Also assigning to a variable makes it visible in the entire scope.” This is a much more complicated rule, which leads to strange situation like outlined above. In fact, we can make it look even more alienating:

function foo()
    try
        x = 2
    catch
    end
    return x
end
foo() # does not work

function bar()
    try
        x = 2
    catch
    end
    x = x
    return x
end
bar() # DOES work

For me this would be something I’d like to see changed in Julia 2.0. Either make it so that a variable needs to be declared before the scope (via e.g. local x which already works btw) or make it like Python, but that while simple is annoying if one uses closures a lot which tends to happen more often in Julia than in Python. So reflecting on this: It’s probably the fact that the declaration/assignment may appear below the inner scope that bothers me, because that means I need to read everything to understand an “embedded scope” and not just everything above that scope.

4 Likes

This does seem weird. Stylistically, I think I would always write this as follows even if this is technically unnecessary.

function bar()
    x = 0
    try
        x = 2
    catch
    end
    x += 1
    return x
end
3 Likes

Or use local to “declare” the variable, which perhaps communicates the intent more clearly:

function bar()
    local x
    try
        x = 2
    catch
    end
    x += 1
    return x
end
2 Likes

I would appreciate this as long the catch also initialized x.

function bar()
    local x
    try
        error("boo!")
        x = 2
    catch
        x = 0
    end
    x += 1
    return x
end
2 Likes

Maybe I’m the weird one here but I’m actually in favor of allowing code to draw on later information. It allows cool things like attribute inference where the attribute of an object with undeclared fields (like in Python) can be inferred by its later uses allowing the object to avoid using dictionary. It also allows type inference or type parameter inference to be more aggressive.

This particular scoping rule can be quite confusing. In particular when combined with anonymous functions. Say you have a vector of vectors, and a function which takes a vector. You want to sum things inside some function:

f::Function
v::Vector{Vector{Float64}}
s = sum(f, v)
... some computations
# set up for some other computation:
a = 4; b = 5; x = 2.3; y = 4.5
g(a, b, x, y, s)

Now, the f takes time, so you want a parallel variant running on chunks of the vector v. You just replace the s = sum(f, v) by:

s = sum(fetch, [@spawn (y = sum(f, vc); @show(y); y) for vc in chunks(v)])

This is admittedly sloppy variable naming, but the net effect is that the program is now incorrect. All the parallel tasks share the same y because it was assigned to later in the function. Sneak in a local y = in the task, and it is correct.

The problem with this is that what you do later in the function affects what happens at the beginning. It’s counter intuitive, but of course, perfectly understandable.

2 Likes

The idea of a @noscope macro for try blocks was explored before, but I lost momentum on it, if someone wants to follow the triage recommendation here Add noscope macro to make try blocks evaluate in the local scope by IanButterworth · Pull Request #39217 · JuliaLang/julia · GitHub

5 posts were split to a new topic: Programming languages with only global scope?

This is something I didn’t know. Thank you!

But that leads me to the following question: Why is it like that? I mean: in my opinion, the scoping behavior of try-catch blocks is more confusing than that of others because it looks like an if-else, so intuitively I would expect it to work like an if-else, but it doesn’t.

2 Likes