-
-
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
RFC: deprecate hard/soft scope distinction #19324
Conversation
I like it and think we should just go ahead and merge all the changes that avoid creating bogus globals in test files. Any idea what the "(much of?)" might refer to in terms of what difference might remain between hard and soft scopes? I don't understand why the begin/end example works – can you elaborate? |
dcf6c8c
to
b919799
Compare
Yes, that's where "much of" is currently not entirely deprecated. I think there's two options here:
Option 1 would mean you could do the following, and begin
x = 1
f() = (x += 1)
end This is the begin/end example above extended to functions (scope inheritance of surrounding global blocks). Option 2 would mean that begin/end blocks would get the same result lowered if lowered as a block or lowered one statement at a time (never scope inherit). This option would also mean that we could also (for example) delay expanding macros, and might help make it easier to get the lowering of I left this undecided in the current parser deprecation. |
So if I understand correctly, the option 1 example you gave would require |
doc/manual/variables-and-scoping.rst
Outdated
@@ -117,24 +128,29 @@ changed within their global scope and not from an outside module. | |||
Note that the interactive prompt (aka REPL) is in the global scope of | |||
the module ``Main``. | |||
|
|||
Within the global scopes, the `global` keyword is never necessary, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps "Within a global scope"?
doc/manual/variables-and-scoping.rst
Outdated
introduced into the top-level scope: | ||
The following rules and examples pertain to local scopes. | ||
A newly introduced variable in a local scope does not | ||
back-propagate to its parent scope. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps "propagate out to its parent scope" or "escape to"?
doc/manual/variables-and-scoping.rst
Outdated
julia> function foobar() | ||
global x = 2 | ||
end; | ||
.. doctest:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doctest appears to belong following the statement "An explicit global
..." on line 253. But due to the deletions above neither foobar
nor x
associated with the remaining parts of this doctest are defined?
begin
x = 0
f() = (global x += 1)
end (I'm not sure if you meant |
So, if I'm understanding this, option 1 makes begin/end blocks and if/else blocks soft. With option 1, how would you write that example without the begin/end block? Would you then need the |
src/jlfrontend.scm
Outdated
(find-assigned-vars ex '() '())))) | ||
lv)) | ||
;; vars assigned anywhere, if they have been been explicitly defined | ||
(gv-implicit (filter (lambda (x) (and (not (or (memq x lv) (memq x gv))) (defined-julia-global x))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this change would mean lowering doesn't depend on defined-julia-global
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The actual change wouldn't, but this is implementing the deprecation to precede the change, so it can't actually alter anything.
Yes, for option 1, if there is no containing soft scope block that assigns to it, then you would have to declare it global somewhere inside the local scope. (editorial note: this usage of "soft scope" is not related to the old behavior, since the difference in scope type is designed by the block construct used (if/begin) rather than the nesting (toplevel/inner). perhaps a new term should be used for clarity?) |
👍 for simplification of the scoping rules. I always found those a bit confusing. However, for x in 1:10
global x
# ...
end looks a bit unintuitive. For readability of the code, I'd always want the |
I was about to suggest the |
@StefanKarpinski I don't follow your |
If we have to write function g()
x = 0
f() = (??? x += 1)
end What keyword (or lack thereof) goes where the |
Currently, |
@StefanKarpinski This PR is limited to the question of what the following code does: for x = 1:10
local function f()
return x += 1
end
global g = f
end The current answer is that you can't know from the information given, since I haven't given you the rest of the scope of |
I agree that's a problem, but if we're going to fix our scoping rules, we probably shouldn't do it in bits and pieces, but rather all at once (or rather, in two deprecation and finalization phases). So you're saying that under both variations of your proposal, no keyword would be required in my example? |
Correct. If we decided eventually to go with an For the |
I don't get your argument that |
The minimum set of scoping rules is global + nesting local scope, and I think you can make an entirely consistent model from this. You only need But again, in this PR I'm primarily just looking at whether we want to continue to have a soft-global-scope, or whether to make global scope always hard-scope. Local scope is already always a soft-scope. Whereas global scope may be hard or soft depending on the scope keyword and the set of names already declared in global scope. |
Generally, +1 to ditching soft/hard scope distinction. However, I think this change would have negative impacts on using Julia for "scripting" (edit: so a -1 from me). A few points:
Example of a future REPL session (or script in global scope, or Jupyter notebook): julia> data = rand(5);
julia> for i=1:length(data)
global data
if data[i]>0.5
data[i] += 2
end
end (edit: this example is wrong as mutation has nothing to do with scope. See @vtjnash post two down for the correct example.) So code will have to be peppered with lots of All in all, I'm not sure whether this is a good change considering Julia is also a scripting language (it would be if REPL/scripting is not important). As the main impact of this PR is on scripting, it makes sense that there was little impact of this PR on base, because that is library code. Whereas the tests are more script-like (although arguably that is bad). One other change could be to keep the hard/soft distinction but make the function scopes properly hard: i.e. they can never write to variable not in its own scope. This would require the |
I think I agree with everything @mauro3 wrote – although, as he said, it's a little hard to understand just what these changes entail. The difference between code you write to mutate a global versus to mutate a local from an outer (function) scope is the primary concern since the workflow of developing with globals in the REPL and then using the same code as part of a function body is ubiquitous – and so far Julia's scoping rules are carefully designed to make almost everything work as similarly as possible going between those two contexts. If we're going to require a keyword to mutate in one case, we should also require a keyword in the other case. Currently we mostly don't require one in either case – a symmetry which the proposed change seems to break. |
Loop variables don't have their own scope rules. This isn't changing your ability to use global data, only the ability to reassign it. If that was so, you may have also forgotten to annotate
|
Just to repeat myself, this PR only impacted test code that wasn't really doing the intended thing. I suspect that the lack of mailing posts is not really that informative. I understand that the intended behavior of the above for loop is not obvious, but that's also why I think it should be made explicit. Here's one of the non-intuitive results that fall out of the current global-soft-scope rules: julia> let
x = 0
g = () -> (global x = 1)
g()
return x
end
0
julia> let
x = 0
g = () -> (global x = 1)
g()
return x
end
1 This also happens to be slow for lowering right now (it seems to be quite difficult for it to figure out the scope of |
Shouldn't this be an error like this already is:
|
No, the function introduces a hard scope |
b919799
to
c997f14
Compare
Let's merge this? |
For me, this PR just shows two files changed. In particular the docs/news are missing. Weren't they around at some point? See e.g. #19324 (review) |
c997f14
to
6772fa6
Compare
Good catch. Looks like I had pushed those to the wrong branch. |
6772fa6
to
e89bb75
Compare
💥 💣 ✊ |
One frustrating consequence of this PR is that I can't simply copy-and-paste code from a function into the REPL for interactive use. e.g. the following typical code is just fine in a function but gives a deprecation warning in the REPL: s = zero(eltype(a))
for x in a
s += x
end Is there some way that this deprecation could be disabled in a REPL context? (If I'm using the REPL to debug/understand code from a function, I want to be able to do bidirectional copy-paste, so being forced to insert |
Yes, this is awkward and why we designed the scope behavior the way it was in the first place – so that local and global scopes worked as similarly as possible. The ways forward I can see here are:
I'm not sure how feasible turning of the deprecation in the REPL is, but it's certainly a thought. |
Ideally, there would be no deprecation here for any loop that occurs in global scope, not just in the REPL. |
Yes, that would be lovely, but then we have the hard/soft scope situation again and we're back where we started, so we can't do that. |
Frankly, this change writing loops interactively much more difficult, and is especially confusing for new users who are used to working transparently with globals. A fix can't be too REPL-specific, since it also has to work for Jupyter, Juno, etcetera. |
This is the only solution that's been proposed that allows the meaning of a given variable to be statically determined. I can imagine not requiring static resolution of variables in the REPL, but it's not great pretty much everywhere else. The technical problems caused by this for inference and compilation are not insignificant, but it's also just not good to not be able to tell without running code what a given variable even refers to. Is it global or local? Who knows! Run the code and see. |
The fundamental issue here is this kind of expression: for _ = 1:1
x = value
end Does this introduce a new In global scope, there is no notion of whether a global variable exists or not independent of whether it has a binding – it either has a value or doesn't. And that's the trouble since whether a global has a binding or not is not a static property, it depends on what previously evaluated code has done. The old behavior was that the above code would update an existing global binding for So we have a choice:
We've gone with option 2 in this PR. Which option would you prefer? |
Realized my analysis was wrong and fixed it. |
How about option 1 in global scope? |
That is what we had. It means that the meaning of |
I'm fine with that. Global loops depend on global state, why not? |
This PR examines the impact of deprecating (much of?) the distinction between hard/soft scope. Instead it simply distinguishes between global and local scope. This means that all scope-blocks introduce the same type of scope (local), rather than distinguishing that toplevel functions have special, hard scope rules. I've updated the manual to try to show how this change would impact the user. The main change is that there would no longer be the concept of implicit globals computed from examining the module bindings. Instead the global/local computation would be purely syntactic. For example, take the following code snippet:
Under the current, this shows
10
, becausex
had a value before the for-loop.Under the new rules, this shows
0
, since the for-loop introduce a new local scope.Making this code work as before would requiring declaring
x
to be a global inside the for-loop scope block:Another option that this PR still permits is to make an assignment to x inside a begin/end block:
The impact to base is small (two corrections to code that will be broken anyways when #265 is fixed).
The impact to tests is larger, as it takes a large number of unintentionally-global variables and causes them to emit a deprecation warning. I think the resulting changes are arguably beneficial, even if we don't decide to change the scoping rules, since they reduce the number of objects being kept around in global variables.