Scoping fun in Julia

Page content

Background

Julia is a high-level programming language that is exceptionally well suited for scientific and mathematical applications. If you are not familiar with it, you should give it a try!

Last year, Julia 1.0 was finally released. Among other important changes, it introduced a different set of scoping rules. For the unwary, these can lead to quite unexpected behavior, and in some cases subtle but embarassing errors (I am speaking out of personal experience here!). What follows is a little example that shows what to watch out for.

Example

Let’s assume we write a function that iteratively updates the state of a system, e.g., given in the form of an array that goes by the name of ‘x’:

function update(x0,eps0)
    x = copy(x0)
    while true
        # x1 = next_state(x)
        x1 = sqrt(x)
        if maximum(abs(x1 - x)) < eps0
            break
        else
            x = x1
        end
    end
    x1
end

What will happen when we run this code?

The return value ‘x1’ on the last line will not be defined, since the variable ‘x1’ is created inside the loop and then goes out of scope after the loop is finished. So this example will return an error:

julia> update(2.0, 1e-6)
ERROR: UndefVarError: x1 not defined

Unfortunately, Julia also supports closures. So if we define ‘x1’ globally before calling ‘update’, the error will be masked:

julia> x1 = 0.0
julia> update(2.0, 1e-6)
0.0

julia>

Related to this is the issue that loop variables will go out of scope after the loop. In other words, you cannot return something through variables created in the loop!

julia> for z = 1:10
       end

julia> println(z)
ERROR: UndefVarError: z not defined
julia> z = -1
-1

julia> for z = 1:10
       end

julia> println(z)
-1.0

But what about the following:

julia> z = -1
-1

julia> for y = 1:10
        z = y
        end

julia> z
-1

Now this is a change made consciously for Julia 1.0 that can lead to some frustration. At the REPL, only variables declared as /global/ variables will be accessible inside scoping blocks! So you need to write:

julia> global z = -1
-1

julia> for y = 1:10
        global z = y
        end

julia> z
10

The reason is, of course, that global variables should be avoided as much as possible. Not only do they pollute the global namespace, but they do not allow the Julia compiler to optimize code for specific variable types. This leads to hugely inefficient code. So it makes sense to make the use of globals highly restrictive.

However, this is inconvenient for scripts, right?

Recommendation

Just make sure to keep in mind the following two things:

  1. Loop-variables cannot be returned directly
  2. Initialize all variables used in a function before first use!
function update(x0,eps0)
    x = copy(x0)
    x1 = 0.0
    while true
        # x1 = next_state(x)
        x1 = sqrt(x)
        if maximum(abs(x1 - x)) < eps0
            break
        else
            x = x1
        end
    end
    x1
end

References

[1] https://github.com/JuliaLang/Julia/issues/28789