Scoping rules for try - catch - finally

I understand that try-catch-finally construct introduces ist own local scope (soft local scope according to https://michaelhatherly.github.io/julia-docs/en/latest/manual/variables-and-scoping.html ). Now I want to do some processing with try/catch/finally AND pass the results to the outer scope. Something like this:

x = 0
y = 0
er = false # or true for another branch

try
    if er
        x = 1
        error()
    else
        x = 2
    end
catch
    x *= 10
finally
    # x *= 100 ### this line would produce an error:
    #### x from try/catch ist not available in finally 
    y = 300
end

println("x=",x, " y=", y)

x =0 y=0

For results from try/catch not available in the finally see discussion in Cumbersome scoping rules for try - catch - finally .

For thist last thing, I could somehow circumwent it by using let block:

let
    x = 0
    y = 0
    er = true # or false for another branch

    try
        if er
            x = 1
            error()
        else
            x = 2
        end
    catch
        x *= 10
    finally
        x *= 100
        y = 300
    end

println("x=",x, " y=", y)
end

println("x=",x, " y=", y)

x=1000 y=300 x=0 y=0

Any idea how to pass the results to the outer scope?

In the first code, those xs and ys inside try catch blocks are all local variables. So global keyword must be prepended to them:

try
    if er
        global x = 1
        error()
    else
        global x = 2
    end
catch
    global x *= 10
finally
    # x *= 100 ### this line would produce an error:
    #### x from try/catch ist not available in finally 
    global y = 300
end

@Sijun thank you!

Do I understand you correctly that in this case x and y are “really global”, i.e. avaliable across all files? If yes, I would give the variables some long obscure name and live with it, but actually it would be nice to have them available in the outer scope of the try block (i.e. in this module/function etc.) only. Could that be done, or is it currently a limitation of the language?

The scope of global variables are limited to the immediate module they belong to.

module OuterModule
   x = 1
   module InnerModule
       x = 2
       function f()
           global x
           println(x) # will print 2
       end
   end

   function f()
     global x
     println(x) # will print 1
   end
end

The following section explains Julia scoping rule in detail:
https://docs.julialang.org/en/v1/manual/variables-and-scoping/

2 Likes

Incidentally, please note that the concept of “soft” and “hard” global scope is now obsolete, there is no such distinction in Julia 1.0. The manual you are using is outdated, use the link by @Sijun.

Generally, Julia underwent a lot of changes before 0.7/1.0, and a lot of material on the web is outdated.

2 Likes

btw: https://github.com/JuliaLang/julia/pull/33864

@Sijun , @Tamas_Papp - thank you for your advice!

@lobingera - well, I began trying to code in Julia just yesterday. If I don’t give up soon, it’s look like I’m in for quite a few surprises.

Mostly good ones, I can guarantee you! Have fun!

1 Like

FWIW, I think that linking discussions like this is more confusing than helpful in the “first steps” category.

2 Likes

@Sijun , according to documentation you cited, global is not limited to the immediate module they belong to:

There are two main types of scopes in Julia, global scope and local scope . The latter can be nested.

Do I understand that correctly?

@Tamas_Papp :grinning: Reading discussions like this was definitely confusing (as confusing as the Julia scoping rules can be) AND helpful for me.

Loops and try-catch creating a local scope may have reasons (with try-catch-finally creating 3 separated scopes being expecially piquant). However normally you would want to pass the results to the “outer world”.

For myself, I’d sum up the following rules to avoid scoping problems:

  1. global is evil. Especially never call loop or try-catch constructs on the global (script) scope
  2. Put everything into functions as early as possible
  3. Otherwise at least put loop or try-catch into a let block
  4. Initialize the variable to be used to return results before the loop or try-catch construct. If the variable starting value is not used in the loop, initialize it with nothing

Like the following

let
    k = nothing
    for i in 1:2
        k = i
    end
    @show k
end

function f2()
    p = 0
    for j in 1:4
        p += j
    end
    return p
end

@show f2()

function tryit(sq)

    ll = mm = nothing
    try
        mm = 9
        ll = sqrt(sq)
    catch
        ll = sqrt(mm)
    finally
        ll += mm
    end
    return ll
end

@show tryit(10.0)
@show tryit("ten")
k = 2
f2() = 10
tryit(10.0) = 12.16227766016838
tryit("ten") = 12.0

As for surprises: The following code works, and I am surprised. The variable soln was not declared anywhere outside of the loop and to my understanding it shouldn’t be available after the loop - still it is available: I then use it as the returned value from the function integrate_ode .

As for the code itself: it resulted from a port of an old Python project to Julia, mainly as an excercise. I measure the execution time at the iteration # 7 to compare with Python / scipy code. BTW it is about 10 times faster (with the potential to reduce the execution time by about a second as compared with Python :wink: )

function integrate_ode(y0, x_arr, m_l_arr, p_arr, ps_arr, signum, mdl_perm_coeff, ppw_opp, iteration)
  loops = 1
  if signum < 0
    x_arr = reverse(x_arr)
  else
    if iteration == 7
      loops = 1000

    end
  end
  params = (x_arr, m_l_arr, p_arr, ps_arr, signum, mdl_perm_coeff, ppw_opp)
  xspan = (x_arr[1], x_arr[end])

  if loops > 1
    eltime = @elapsed begin
      for cnt in 1:loops
        problm = ODEProblem(own_tmq_m_w, y0, xspan, params)
        soln = solve(problm,reltol=1e-6,saveat=x_arr)
      end
      # about 3 to 4 ms per loop
    end
    @show eltime
  else
    problm = ODEProblem(own_tmq_m_w, y0, xspan, params)
    # @show y0
    soln = solve(problm,reltol=1e-6,saveat=x_arr)
  end
  if signum < 0
    return reverse(soln(x_arr).u)
  else
    return soln(x_arr).u
  end
end
module Outer
    x = 1
    module Inner
        import ..Outer
        x = 2
        f() = println(x)
        g() = println(Outer.x)
    end
    f() = println(x)
    g() = println(Inner.x)
end

Outer.f() # 1
Outer.g() # 2
Outer.Inner.f() # 2
Outer.Inner.g() # 1

x defined in module Outer is not accessible to module Inner by the name x alone.
x must be qualified by Outer (as Outer.x) and vise versa.

Unless qualified by appropriate module name, a variable defined in one module is not visible to other modules and (I think) can be said to be limited only to its immediate module it belongs to (so no name clash can occur).

1 Like

I am not so sure that I would do this via outer variables: normally I would either rethrow an error, or return a value.

function foo()
    loop = 10
    if loop > 1
        for i in 1:10
            soln = 1
        end
    else
        soln = 2
    end

    println(soln)
end

Because of the soln = 2 in else block, the above code is equivalent to this:

function foo()
    loop = 10
	local soln # soln is function-local
    if loop > 1
        for i in 1:10
            soln = 1
        end
    else
        soln = 2
    end

    println(soln)
end

And so the soln inside for-loop refers to soln local to function foo().

I am not sure I understood you.

try
    x = 2*2
catch e
    swear(e)
end

Now, should nothing “exceptional” happen, I need the value of x. How do I get access to it?

@Sijun thank you for your explanation on modules scope

If no exceptions are thrown, the value of the above expressions is just 4. Just use it directly. No need to assign it to x.

Thank you! So the code would be (yes, it works!)

x = try
    2*2
catch e
    swear(e)
end

@show x # x = 4

The same is valid for let. The same is NOT valid for loops :roll_eyes:

for loops have value nothing, so yes, use them for side effects.