Open simonbyrne opened 11 months ago
The scoped value being constant within a particular scope is an important semantic piece. I would loathe to give that up.
The REPL could manage the scope, but Intentionally didn't expose a setscope!
function since it makes it much harder to reason about the extent of a scope.
So REPL could have a push/pop
scope stack, but I am not sure that it's worth it.
The scoped value being constant within a particular scope is an important semantic piece. I would loathe to give that up.
I agree, though the pattern I outlined above is effectively doing that via other means. I don't think it is something we should encourage.
The REPL could manage the scope, but Intentionally didn't expose a
setscope!
function since it makes it much harder to reason about the extent of a scope.
You mean a function which changes the scope of the current task? That's sort of what I want: can you expand on what your concerns are? Is it that you want the compiler to be able to assume that scoped values are unchanged outside of with
blocks?
What if we had something weaker, e.g. changes to the scope would only be reflected at top-level (similar to how eval works)? I think that would do what I want?
So REPL could have a
push/pop
scope stack, but I am not sure that it's worth it.
How would this be easier to reason about?
You mean a function which changes the scope of the current task? That's sort of what I want: can you expand on what your concerns are? Is it that you want the compiler to be able to assume that scoped values are unchanged outside of with blocks?
Yeah pretty much. I want to leave the door open for #51352
What if we had something weaker, e.g. changes to the scope would only be reflected at top-level
Maybe? That's what I was thinking with push
/pop
(in terms of how this work in bash). It would need to be semantically equivalent for "restarting" the repl within a new scope.
I agree, though the pattern I outlined above is effectively doing that via other means. I don't think it is something we should encourage.
Yeah, it's not something we can stop people from doing :) You could also put a Ref in there and mutate it directly. Then you directly opt out of the const semantics.
I could see there also being some advantage to allowing something similar in scripts as well, e.g. setting the logger at the start of a program.
I do see the appeal, but I am unsure about the semantics and the implications.
The overall problem is when to restore the previous scope, what is scope exit.
I honestly don't mind making precision a Ref, and then set precision modifying the ref within the current scope.
You still limit the effect of that operation and that's a boon
How about this: make @setscope!
a macro that can only be called at top-level?
macro setscope!(exprs...)
Expr(:toplevel, quote
ct = Base.current_task()
ct.scope = Base.ScopedValues.Scope(ct.scope, $(map(esc, exprs)...))
end)
end
then this would prevent it being called inside a function
julia> function setprec(n)
@setscope!(Base.MPFR.CURRENT_PRECISION => n)
end
ERROR: syntax: "toplevel" expression not at top level
Stacktrace:
[1] top-level scope
@ REPL[9]:1
We could either then implement setprecision
as a macro as well? Or have setprecision
implemented via eval
:
julia> macro setscope!(exprs...)
Expr(:toplevel, quote
ct = Base.current_task()
ct.scope = Base.ScopedValues.Scope(ct.scope, $(map(esc, exprs)...))
end)
end
@setscope! (macro with 1 method)
julia> function setprec_eval(n)
@eval (@setscope!(Base.MPFR.CURRENT_PRECISION => $n))
end
setprec_eval (generic function with 1 method)
julia> precision(BigFloat)
256
julia> setprec_eval(128)
Base.ScopedValues.Scope(ScopedValue{Int64}@0x17c8c76e2041a828 => 128)
julia> precision(BigFloat)
128
though I'm not sure this would give the semantics you had in mind.
I don't like the use eval there, but making it a macro also seems odd.
I think my issue is that the lifetime/extent of the ScopedValue is ambiguous, but I guess setscope
is probably fine (even as a function). Since it just says add this value to the dynamic scope until you exit it, which generally is a fine operation. The manual exit operation is the one that could break things, since you need reference to the previous scope.
I think a good way to implement this would be to have the REPL enter a normal with
block and run a nested REPL that you can exit with ^D.
Aside: one thing I hate about nested REPLs that you exit with ^D is that it makes it really easy to accidentally exit your whole REPL session.
Looking at how
ScopedValue
s are used for the logger (#50958) and my proposed use for MPFR precision and rounding (#51362), we've ended up with a pattern where if theScopedValue
isn't defined, we fall back on a globalRef
value which can be set globally, defining an accessor like:If this is going to be common, it seems like a clunky pattern. One straightforward solution would be to make the
default
field mutable (which would remove the need for this pattern), but it is perhaps worth considering why this is used.@vchuravy asked if it was important if we be able to set the precision globally. I mentioned backward compatibility, but the primary rationale is for interactivity: if I want to increase the precision for all my last calculation, I can just call
setprecision(BigFloat, 1024)
, and then re-evaluate the expression (hit up arrow twice, and return). If I didn't have this ability, I would have to wrap everything expression insetprecision(BigFloat, 1024) do .... end
, which is fiddly and time consuming.However, if we had some way to evaluate inside a given scope at the REPL, this might not be necessary: basically what I want is something similar to the contextual module REPL, but for scopes.