-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Global variable scope rules lead to unintuitive behavior at the REPL/notebook #28789
Comments
Stefan suggested here that one possibility to solve this issue is automatic wrapping of REPL entries in |
But wouldn't that be confusing in that you couldn't do
and use |
The behavior wouldn't be just to wrap everything in a |
So you would turn for i in 1:2
before = false
end would be turned into this: before = let before = before
for i in 1:2
before = false
end
end Frankly, I'm pretty annoyed that people are only giving this feedback now. This has change has been on master for ten months. |
I'm guilty of not having followed master very closed until recently, so this feedback is indeed a bit late. More than a concern for programmers (most Here it becomes a bit difficult to teach a beginner how to sum numbers from 1 to 10 without explaining functions or global variables. |
To be fair, Julia 0.7 was released 13 days ago. This is a new change for most Julia users. |
Unfortunately for those of us who can not handle living on the edge, its brand-new from our perspective. |
And for those of us who have been encouraged to stay off the development branches, "it's brand-new from our perspective." |
Can we please go back to focus on the issue at hand now, instead of having a meta discussion about how long people have had to test this. It is what it is right now, so let's look forward. |
This is a big point. After finding out what the issue really is, it's surprising how little it actually shows up. It is less of an issue with a lot of Julia code in the wild and in tests, and it did reveal a lot of variables which were accidentally global (in both Julia Base's tests according to the original PR, and I noticed this on most of DiffEq's tests). In most cases it seems that the subtly wrong behavior isn't what you get (expecting a change in a loop), but rather expecting to be able to use a variable in a loop is what I've found to be the vast majority of where this shows up in updating test scripts to v1.0. So the good thing is that in most cases the user is presented with an error, and it's not difficult to fix. The bad thing is that it's a little verbose to have to put I for one would like to see the experiments with global x = 5
for i = 1:5
println(x+i)
end could be a nice way to keep the explicitness, and would make the "REPL code is slow because of globals" be much more obvious. The downside is that once again throwing things into a function would not require the But given how this tends to show up, it's not really gamebreaking or a showstopper. I'd classify it as a wart that should get a mention in any workshop but it's not like v1.0 is unusable because of it. I hope that changing this behavior isn't classified as breaking and require v2.0 though. |
I'm not so sure I like the idea that the REPL should behave like a function interior. It clearly isn't, so I expect it to behave like global scope. To me the REPL not behaving like global scope would be potentially even more confusing than the discrepency that causes this issue. Regardless, at the very least I think that the documentation should be somewhat more explicit about this issue. Casually reading the docs I would have assumed that you would need to use the |
If we're going for "REPL is the same as the inside of a function" we should also think about julia> i = 1
1
julia> for outer i = 1:10
end
ERROR: syntax: no outer variable declaration exists for "for outer" versus: julia> function f()
i = 0
for outer i = 1:10
end
return i
end
f (generic function with 1 method)
julia> f()
10 |
People haven't been using master for interactive use or for teaching, they've been using it to upgrade packages, which are only minimally affected by this and are mostly written by experienced programmers. (I was one of the few people who did give feedback in #19324, though, where I argued for the old behavior.) A non-breaking way out of this would be to change back to the old behavior (ideally not by inserting implicit |
Yes, and I greatly appreciate it. It doesn't much matter now since we made the choice, let it bake for ten months and have now released it with a long-term commitment to stability. So the only thing to do now is to focus on what to do going forward. Having an option to choose between the old behavior and the new one is interesting but it feels very hacky. That means we not only sometimes have a scoping behavior that everyone apparently found incredibly confusing, but we don't always have it and whether we have it or not depends on a global flag. That feels pretty unsatisfactory, I'm afraid. |
If someone implements an "unbreak me" soft-scope AST transformation, it will be very tempting to use it in IJulia, OhMyREPL, etcetera, at which point you get the even more problematic situation in which the default REPL is seen as broken. |
That's not what I'm saying. Clearly we should use the same solution in all those contexts. But implementing it as two different variations on scoping rules seems less clean than implementing it as a code transformation with one set of scoping rules. But perhaps those are functionally equivalent. However, it seems easier to explain in terms of the new simpler scoping rules + a transformation that takes REPL-style input and transforms it before evaluating it. |
That could be done as |
I actually started looking into implementing something like this a few minutes ago. However, it looks like it would be much easier to implement as an option in
I seriously doubt this would be easier to explain to new users than just saying that the rules are less picky for interactive use or for |
Here is a rough draft of a |
Okay, I've figured out how to implement a A possible (non-breaking) way forward, if people like this approach:
Or is it practical to roll it into REPL.jl immediately? I'm not completely clear on how stdlib updates work in 1.0. Please take a look at my implementation, in case I'm missing something that will cause it to be fragile. |
Can't we have it as a non-default feature of the REPL in 1.1? |
Duplicate of #28523 and #28750. To those saying they don't want to teach people about global variables, I suggest teaching functions first, before Adding a non-default feature to the REPL for this seems ok to me though. |
@JeffBezanson, remember that many of us would like to use Julia as a substitute for Matlab etcetera in technical courses like linear algebra and statistics. These are not programming courses and the students often have no programming background. We never do structured programming — it's almost all interactive with short snippets and global variables. Furthermore, the reason I'm using a dynamic language in the first place is to switch fluidly between interactive exploration and more disciplined programming. The inability to use the same code in a global and a function context is a hindrance to that end, even for someone who is used to scoping concepts, and it is much worse for students from non-CS backgrounds. |
Many of us Julia users have absolutely 0 CS background (including myself), but it seems to me that the proper attitude (especially for students) is a willingness to learn rather than demanding things be changed for the worse to accommodate our naivete. Now, I'm not necessarily implying that this particular change would be for the worse as I only have a limited understanding of what's going on here, but if it is the case that this is a significant complication or makes it excessively easy to write needlessly badly performing code it does not seem worth it to make a change in order to have a better lecture example. You can't change the laws of physics so that the electrostatics examples you show to freshman are more applicable to real life. So my question as a non-CS user who also cares about performance is how would I be likely to screw up if this were made the default behavior. Is it literally just the sorts of examples we are seeing here that are a problem (which I was already aware of), or are we likely to often screw this up badly in more subtle ways? For what it's worth, I do agree that having code behave differently depending on its enclosing scope is a generally undesirable feature. |
Making code harder to write interactively, forcing beginners writing their first loops to understand obscure scoping rules, and making code pasted from functions not work in global scopes does not help programmers write fast code in functions. It just makes it harder to use Julia interactively and harder for beginners. |
Making an "unbreak me" option the default seems wiser, especially an option that is aimed squarely at beginning users. If it is a non-default option, then precisely those people who need it most will be those who don't have it enabled (and don't know it exists). |
What would the proposed REPL-mode do to |
If we did something like this it seems like it might make sense for the module to determine how it works. So |
I was interested to see if it was possible to monkey patch the REPL to use @stevengj's I'm not going to be recommending this to people, but it's quite useful to me when doing quick-and-dirty data analysis. I would very much like to see a (better thought out) solution since it really is quite confusing for inexperienced and often reluctant coders (i.e., my students) when you can't copy and paste in code from a function into the REPL to see what it does and vice-versa. julia> a = 0
0
julia> for i = 1:10
a += i
end
ERROR: UndefVarError: a not defined
Stacktrace:
[1] top-level scope at .\REPL[2]:2 [inlined]
[2] top-level scope at .\none:0
julia> using SoftGlobalScope
[ Info: Precompiling SoftGlobalScope [363c7d7e-a618-11e8-01c4-4f22c151e122]
julia> for i = 1:10
a += i
end
julia> a
55 (BTW: the above is about as much testing as it has had!) |
For that matter, it's not crazy to me to default to |
That. The other one to seriously consider is Juno. Remember that people will It sure sounds to me like the best solution is that the hard-scope is simply an opt-in thing, where if every other usage (including |
Do you want to write
That is not how this works. You can't get any change to the language you want just by calling the current behavior a bug. I reeeally don't think there should be a command line option for this. Then every piece of julia code will have to come with a comment or something telling you which option to use. Some kind of parser directive in a source file would be a bit better, but even better still would be to have a fixed rule. For example, hard scope inside modules only might make sense. Let me try again to provide an explanation of this that might be useful for avoiding the mania, hysteria, and carnage people are seeing in the classroom: " Perhaps that can be improved; suggestions welcome. I know, you'd rather not need any sort of explanation at all. I get that. But it doesn't seem so bad to me. |
I agree. Sounds like a teaching and communication headache to me.
Just so I understand: if I had a short script (not in a module!) in a If so, that makes complete sense,is very teachable and coherent. Top-level scripts are an interactive interface for exploration, etc. but you would never put that kind of code in a module. Modules are something that you should fill with functions are very carefully considered globals. It would be easy to tell people about those rules. |
No, I'd rather not! But scripting languages that have a REPL rarely do that (e.g. ruby, python, R, ...), they behave like Julia v0.6 did.
I completely understand what you're saying here, and I won't (touch wood!) make this mistake again. But the whole problem I'm worried about is not me. I've found it relatively easy to introduce scope (without mentioning it directly) when I explain that variables inside functions can't see ones outside and vice-versa (even though that's more an aspiration than a fact in R!), because functions themselves are already a relatively advanced concept. But this hits much earlier in the learning curve here where we don't want anything remotely as complicated as scope to be impinging on people... Note also it's not just "variables you introduce in the REPL or at the top level, outside of anything else, are global" and "variables introduced inside functions and loops are local", it's also that variables in if statements in the REPL or at the top level are global but variables in a However, I agree with @jlperla - the proposal that "hard scope inside modules only might make sense" seems completely fine to me! Modules are a sufficiently advanced concept again... if soft scope works for the REPL and scripts, that's absolutely fine. |
What I'm trying to get at is that I feel a simple description of global vs. local is sufficient for early-stage teaching --- you don't even need to say the word "scope" (it does not occur at all in my explanation above). When you're just showing some simple expressions and loops in the REPL, you're not teaching people about testsets and you don't need an exhaustive list of the scoping behavior of everything in the language. My only point is, this change does not suddenly make it necessary to teach lots of details about the language up front. You can still ignore the vast majority of stuff about scopes, testsets, etc., and a simple line on global vs. local should suffice. |
In a world where everyone started writing all of their code from scratch, I would agree completely. The issue is that you need to teach students not just about scope, but also about understanding the scope of where they copy-pasted code they got from. You need to teach them that if they copy-paste code that is on stackexchange within a function or a let block that they need to scan through it and find where to add "global" if they are pasting it into the REPL or a And then students start asking why does |
Pop quiz: in julia 0.6, is
The answer is that there's no way to know, because it depends on whether a global |
Folks, this discussion is verging on no longer being productive. Jeff knows very well that the old behavior was nice in the REPL. Who do you think designed and implemented it in the first place? We have already committed to changing the interactive behavior. A decision still needs to be made about whether a "script" is interactive or not. It sounds interactive when you call it "a script" but it sounds far less interactive when you call it "a program"—yet they are exactly the same thing. Please keep the replies short and constructive and focused on the things which still must be decided. If there's comments that deviate from this, they may be hidden and the thread may be locked. |
One thought that I had but we dismissed as being "too annoying" and "likely to cause the villagers to get out their pitchforks" was that in non-interactive contexts, we could require a |
When I was first introduced to Julia (not a long time ago, and I come from a Fortran background mostly), I was taught that "Julia is compiled and fast at the function level, thus everything that must be efficient must be done inside functions. In the main 'program' it behaves like a scripting language". I found that fair enough, as I cannot imagine anyone doing anything too computationally demanding without understanding that statement. Therefore, if there is any sacrifice in performance at the main program for using the same notation and constructions than in the functions, I find that totally acceptable, much more acceptable than trying to understand and teach these scoping rules and not being able to copy and paste codes from one place to another. By the way, I am a newbie in Julia yet, having chosen it to teach some high-school and undergraduate students some basics of simulations of physical systems. And I am already hopping this issue returns to the 'normal' behavior of previous versions, because it gives us quite a headache. |
This conversation is locked now and only Julia committers can comment. |
@JeffBezanson, what would be the plan to implement the semantics you suggested in this discourse thread, initially only in the REPL and opt-in elsewhere? It sounds like you are planning to put that directly into the lowering code ( |
warn when an implicit local at the toplevel shadows a global fixes #28789
warn when an implicit local at the toplevel shadows a global fixes #28789
warn when an implicit local at the toplevel shadows a global fixes #28789
warn when an implicit local at the toplevel shadows a global fixes #28789
warn when an implicit local at the toplevel shadows a global fixes #28789 - make `let` always a hard scope, and use it in testsets - suppress side effects (warnings) from lowering unless we are going to eval the result immediately
warn when an implicit local at the toplevel shadows a global fixes #28789 - make `let` always a hard scope, and use it in testsets - suppress side effects (warnings) from lowering unless we are going to eval the result immediately
warn when an implicit local at the toplevel shadows a global fixes #28789 - make `let` always a hard scope, and use it in testsets - suppress side effects (warnings) from lowering unless we are going to eval the result immediately
Example 1
This came up with a student who upgraded from 0.6 to 1.0 directly, so never even got a chance to see a deprecation warning, let alone find an explanation for new behavior:
Example 2
I "get" why this happens in the sense that I think I can explain, with sufficient reference to the arcana in the manual about what introduces scopes and what doesn't, but I think that this is problematic for interactive use.
In example one, you get a silent failure. In example two, you get an error message that is very there-is-no-spoon. Thats roughly comparable to some Python code I wrote in a notebook at work today.
I'm not sure what the rules are in Python, but I do know that generally you can't assign to things at the global scope without invoking global. But at the REPL it does work, presumably because at the REPL the rules are different or the same logic as if they were all are in the scope of function is applied.
I can't language-lawyer the rules enough to propose the concrete change I would like, and based on Slack this isn't even necessarily perceived as an issue by some people, so I don't know where to go with this except to flag it.
Cross-refs:
#19324
https://discourse.julialang.org/t/repl-and-for-loops-scope-behavior-change/13514
https://stackoverflow.com/questions/51930537/scope-of-variables-in-julia
The text was updated successfully, but these errors were encountered: