Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confusing documentation for scope rules. #40238

Closed
yygrechka opened this issue Mar 27, 2021 · 5 comments · Fixed by #40394
Closed

Confusing documentation for scope rules. #40238

yygrechka opened this issue Mar 27, 2021 · 5 comments · Fixed by #40394

Comments

@yygrechka
Copy link

yygrechka commented Mar 27, 2021

The documentation claims the following:

A new local scope is introduced by most code blocks (see above table for a complete list). Some programming languages require explicitly declaring new variables before using them. Explicit declaration works in Julia too: in any local scope, writing local x declares a new local variable in that scope, regardless of whether there is already a variable named x in an outer scope or not. Declaring each new local like this is somewhat verbose and tedious, however, so Julia, like many other languages, considers assignment to a new variable in a local scope to implicitly declare that variable as a new local.

https://docs.julialang.org/en/v1/manual/variables-and-scoping/#Local-Scope

Following my discussion here: https://discourse.julialang.org/t/referencing-local-variable-before-assignment-results-in-unexpected-behavior/58088/2, I'm given to understand that when functions are nested, inner functions can overwrite variables in the scope of the outer function.

This paragraph in the documentation makes it sound that the statement x = 1 is equivalent to local x = 1; I initially understood "new local"/"new variable" to mean a variable that is new to the scope that it is defined into.

In the proceeding paragraph which seeks to spell out the exact rules for what happens when x = <value>, we have the following line:

Existing local: If x is already a local variable, then the existing local x is assigned;

However, it is not clear if Existing local refers to the existing local within a particular scope block or within the outermost scope block.

e.g. consider the following code:

function f()
    x1 = 0
    function f2()
        x1 = 3 
    end
    f2()
    return x1
end

println(f())

After engaging with Julia discourse, I understand that calling f2 will modify the x1 variable in the outermost scope, and so f() will return 3, however, after I initially read the documentation, I came out with the impression that calling f2() should not affect x1 in the outermost scope, and thus my initial impression was that f() should return 0.

@yygrechka yygrechka changed the title Wrong documentation for scope rules. Confusing documentation for scope rules. Mar 30, 2021
@StefanKarpinski
Copy link
Member

StefanKarpinski commented Mar 30, 2021

assignment to a new variable in a local scope to implicitly declare[s] that variable as a new local

In other words, If an assignment to a variable occurs in a local scope and there is no variable with that name visible in that scope, then the variable is implicitly declared as a new local in that scope. This is always true. In all of your examples, there is an existing local that is visible from an outer scope, so the antecedent doesn't apply.

Existing local: If x is already a local variable, then the existing local x is assigned;

However, it is not clear if Existing local refers to the existing local within a particular scope block or within the outermost scope block.

It doesn't matter which one, the rule applies either way: if x is a local variable in this scope, then that binding is updated; if it is an existing local in some containing local scope, then that outer binding is updated. If you can think of some edit that would clarify this without making it too much more verbose, suggestions would be welcomed.

Of course things like scope are tricky to understand from rules like this, which is why there are also examples in the rest of the section. In particular, you'll note that the sum_to function displays exactly the behavior in question: s is local to the outermost local scope of the function; it is assigned in the body of the for loop, which has its own local scope but does not introduce a new s variable. It's possible that this example does not hit that hard for people accustomed to Python, since in Python the behavior is the same for a very different reason: in Python the for loop doesn't introduce a new scope. It's possible that it would be good to have an example with an inner function even though that behaves exactly the same way as the for loop example.

@yygrechka
Copy link
Author

yygrechka commented Mar 30, 2021

Yes, I looked at the sum_to example as well after the discourse discussion; I agree that having an inner function would be more helpful.

It doesn't matter which one, the rule applies either way: if x is a local variable in this scope, then that binding is updated; if it is an existing local in some containing local scope, then that outer binding is updated.

It matters what Existing local refers to; because if it specifically refers to a variable within a particular scope, then the variable in the outer scope shouldn't get updated, and the second clause applies:

Hard scope: If x is not already a local variable and assignment occurs inside of any hard scope construct (i.e. within a let block, function or macro body, comprehension, or generator), a new local named x is created in the scope of the assignment

I would rephrase the initial paragraph to the following:


A new local scope is introduced by most code blocks (see above table for a complete list). Some programming languages require explicitly declaring new variables before using them. Explicit declaration works in Julia too: in any local scope, writing local x declares a new local variable in that scope, regardless of whether there is already a variable named x in an outer scope or not. Foregoing the local keyword results in the same behavior as if a variable named x does not exist in any outer scope. However, if x does exist in an outer scope then the corresponding variable in the inner scope acts as a reference to its outer-scope counterpart. (i.e. executing x=<value> in the inner scope will update the variable x in the outer scope as well.)
.....
Existing local: If x is already a local variable in the current or any outer scope, then the existing local x is assigned;

Hard scope: If x is not already a local variable in the current or any outer scope, and assignment occurs inside of any hard scope construct (i.e. within a let block, function or macro body, comprehension, or generator), a new local named x is created in the scope of the assignment;

Soft scope: If x is not already a local variable in the current or any outer scope, and all of the scope constructs containing the assignment are soft scopes (loops, try/catch blocks, or struct blocks), the behavior depends on whether the global variable x is defined:
etc...


Let me know what you think.

@StefanKarpinski
Copy link
Member

It matters what Existing local refers to; because if it specifically refers to a variable within a particular scope, then the variable in the outer scope shouldn't get updated, and the second clause applies:

No, that's not the case. You can completely ignore hard/soft scope unless you're in the REPL or trying to shadow a global variable with the same name as a local variable. It does not matter whether the existing local is in this scope or an enclosing one, if it exists it is assigned to. Period, end of story.

Comments on the edit suggestions...

Foregoing the local keyword results in the same behavior as if a variable named x does not exist in any outer scope.

This edit is incorrect: if the local keyword is omitted, assigning to x does not behave as if x does not exist in any outer scope. In fact, it's exactly the opposite: if x already exists in an outer local scope, then assigning to it updates the existing outer x.

However, if x does exist in an outer scope then the corresponding variable in the inner scope acts as a reference to its outer-scope counterpart. (i.e. executing x=<value> in the inner scope will update the variable x in the outer scope as well.)

This description is not how local variables or scopes work. Inner variables do not act as references to outer variables. The only question when it comes to matters of scope is "When I write x in two places, are they the same x or different?" When you assign to x it affects all the places that refer to the same x and has no effect on places that refer to a different x.

Existing local: If x is already a local variable in the current or any outer scope, then the existing local x is assigned;

This edit does help clarify, we can make that change. The rest of the edits seem to do the same thing and add the phrase "in the current or any outer scope" after "already a local variable", which I guess helps clarify but it feels like we could maybe make this simpler by just explaining once that when we say that a local exists, that's what we mean.

@heliosdrm
Copy link
Contributor

I have the feeling that the confusion might be caused by the very first statement of the paragraph: "A new local scope is introduced by most code blocks". That "new local scope" might be understood as a local scope disconnected from the outer one, such that it does not matter if x was already a local outside that new scope. Might it be clearer if it extended as follows?

A new local scope is introduced by most code blocks (see above table for a complete list). This new scope contains the existing local variables if the block is inside another local scope, regardless of whether they are defined before or after the new code block.

The last sentence is not directly related to the point made by @yygrechka, but I think it might also be a source of confusion.

@yygrechka
Copy link
Author

Yes, I like the edit that @heliosdrm proposes. That would make things clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants