Scope in functions

One of my collaborators found the following behaviour confusing.

function foo()
  function bar()
    x = 3
    return x
  end
  x = 1
  bar()
  return x
end

foo()

which outputs 3 (and not 1). This behaviour is different to some other common languages.

Is this related to “An assignment introducing a variable used inside a function, type or macro definition need not come before its inner usage…” in

https://docs.julialang.org/en/v1/manual/variables-and-scoping/index.html ?

In global scope, if one executes

function bar()
  x = 3
  return x
end
x = 1
bar()
x

then 1 is the output, not 3.

1 Like

In your first example x is a local variable and all three instances refer to the same x. In you second example, x = 3 creates a local variable and x = 1 a global. If you wanted the former to be global you’d have to state it global x = 3, then the output would be 3 again.

1 Like

Thanks. I understand that ultimately all the instances refer to the same x, from the output.

The confusing part is that in the first example, the x in bar() actually modifies the x that is assigned later in the same function. This is different behaviour to some other languages, e.g. R.

Also, if one was to write

function foo()
  function bar()
    local x = 3
    return x
  end
  x = 1
  bar()
  return x
end

foo()

then the output is 1, which is what some might expect from the first snippet as well.

I don’t see why they should be similar — they are different languages, and in particular R has a very, ehem, interesting attitude to scope that few other languages copy, and even seasoned R users do not particularly like.

What the… I didn’t know about that :see_no_evil:

No, in your second example the two x do not refer/bind to the same thing.

Hi, thanks for the responses. I do understand that the second example is different; that’s why I provided it.

I was not saying that the behaviour should be the same, or trying to start a discussion on differences in scoping more broadly, although I’m sure it would be interesting. I was just pointing out that the behaviour is different, so it is natural to want to understand why.

My question was:

Is this related to “An assignment introducing a variable used inside a function, type or macro definition need not come before its inner usage…” in

Scope of Variables · The Julia Language ?

No, it’s that global variables bindings you cannot change (unless you specify that you want it with a global; otherwise a new local variable is introduced on assignment) whereas local variable bindings you can change.

Just for comparison, I’ve tried Python3 and Ruby for similar codes:

def foo():
    def bar():
        x = 3
        return x
    #
    x = 1
    bar()
    print( "python: in foo():", x )
#

foo()
def bar():
    x = 3
    return x
#

x = 1
bar()
print( "python3: top level:", x )
def foo()
    def bar()
        x = 3
        return x
    end
    x = 1
    bar()
    puts( "ruby: in foo(): ", x )
end

foo()
def bar()
    x = 3
    return x
end
x = 1
bar()
puts( "ruby: top level: ", x )

all of which give x = 1 (I hope the syntax above is correct though!) So I guess some people from those languages may also expect x to be 1. In my case, I use the “local” keyword whenever I’m not sure about the interpretation…

Python would be:


def foo():
    def bar():
        nonlocal x
        x = 3
        return x
    x = 1
    bar()
    print( "python: in foo():", x )
1 Like

Yes, it’s related to that. The scope of variables depends on the existence of assignments and local declarations in the scope, not on their order. This is intended to make scope more predictable; you can reorder statements within the same scope without changing the scopes of variables.

We also (like Scheme) allow assignments in inner functions to overwrite local variables in enclosing functions by default.

3 Likes

In general we try hard to make meaning independent of order in a couple of different senses.

First, we strongly avoid making meaning depend on order of execution because it is inherently unpredictable. In this case it’s pretty simple but in general it is intractable (in the formal sense) to decide what will execute before what without actually executing it. Traditional dynamic languages have often been fine with allowing actual execution to determine semantics because they do almost no static analysis of code. A principle in the design of Julia is that the meaning of code should be statically resolvable, which helps both compilers and humans trying to understand the code.

Second, we also try to avoid syntactic order being significant where order is not inherently significant (c.f. it’s inherent to the order of expression evaluation). For example, we’ve been very careful to design things so that the order of import statements doesn’t matter (or more precisely, if you have a program that works without warnings and you reorder the import statements and the program still works without warnings, then it does the same thing). We’ve also been very careful to design things so that the order of method definitions doesn’t matter. This is because people like to reorganize their code by moving things around. And if that can cause subtle changes in meaning, that’s a huge gotcha.

The scope rule is very simple: assignment to x in a local scope means either

  • if an x local variable exists in an outer scope, it updates it;
  • if no x local variable exists, it creates and assigns x.

It does not matter if a local x in an outer scope is created or assigned before or after the assignment to x that we’re considering the meaning of—in both the syntactic and evaluation order sense of the word. In particular, this also means that these don’t affect the meaning of the code:

  • if the assignment x = 1 is syntactically before or after the definition of bar
  • if the assignment x = 1 is evaluated before or after the definition of bar

This means that you can move code around safely and put assignments in conditionals without fear of subtle changes of meaning.

6 Likes

Thanks very much for the detailed explanation!