-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Scoped values #50958
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
Scoped values #50958
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -163,7 +163,7 @@ | |
# result::Any | ||
# exception::Any | ||
# backtrace::Any | ||
# logstate::Any | ||
# scope::Any | ||
# code::Any | ||
#end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -648,6 +648,11 @@ export | |
sprint, | ||
summary, | ||
|
||
# ScopedValue | ||
with, | ||
@with, | ||
ScopedValue, | ||
|
||
# logging | ||
@debug, | ||
@info, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
# This file is a part of Julia. License is MIT: https://julialang.org/license | ||
|
||
module ScopedValues | ||
|
||
export ScopedValue, with, @with | ||
|
||
""" | ||
ScopedValue(x) | ||
|
||
Create a container that propagates values across dynamic scopes. | ||
Use [`with`](@ref) to create and enter a new dynamic scope. | ||
|
||
Values can only be set when entering a new dynamic scope, | ||
and the value referred to will be constant during the | ||
execution of a dynamic scope. | ||
|
||
Dynamic scopes are propagated across tasks. | ||
|
||
# Examples | ||
|
||
```jldoctest | ||
julia> const sval = ScopedValue(1); | ||
|
||
julia> sval[] | ||
1 | ||
|
||
julia> with(sval => 2) do | ||
sval[] | ||
end | ||
2 | ||
|
||
julia> sval[] | ||
1 | ||
``` | ||
|
||
!!! compat "Julia 1.11" | ||
Scoped values were introduced in Julia 1.11. In Julia 1.8+ a compatible | ||
implementation is available from the package ScopedValues.jl. | ||
""" | ||
mutable struct ScopedValue{T} | ||
const has_default::Bool | ||
const default::T | ||
ScopedValue{T}() where T = new(false) | ||
ScopedValue{T}(val) where T = new{T}(true, val) | ||
ScopedValue(val::T) where T = new{T}(true, val) | ||
end | ||
|
||
Base.eltype(::ScopedValue{T}) where {T} = T | ||
|
||
""" | ||
isassigned(val::ScopedValue) | ||
|
||
Test if the ScopedValue has a default value. | ||
""" | ||
Base.isassigned(val::ScopedValue) = val.has_default | ||
|
||
const ScopeStorage = Base.PersistentDict{ScopedValue, Any} | ||
|
||
mutable struct Scope | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this mutable? I don't see any place where we actually mutate it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah that's an artifact from an earlier implementation, should be changed or values should be |
||
values::ScopeStorage | ||
end | ||
|
||
function Scope(parent::Union{Nothing, Scope}, key::ScopedValue{T}, value) where T | ||
val = convert(T, value) | ||
if parent === nothing | ||
return Scope(ScopeStorage(key=>val)) | ||
end | ||
return Scope(ScopeStorage(parent.values, key=>val)) | ||
end | ||
|
||
function Scope(scope, pairs::Pair{<:ScopedValue}...) | ||
for pair in pairs | ||
scope = Scope(scope, pair...) | ||
end | ||
return scope::Scope | ||
end | ||
Scope(::Nothing) = nothing | ||
|
||
""" | ||
current_scope()::Union{Nothing, Scope} | ||
|
||
Return the current dynamic scope. | ||
""" | ||
current_scope() = current_task().scope::Union{Nothing, Scope} | ||
vchuravy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function Base.show(io::IO, scope::Scope) | ||
print(io, Scope, "(") | ||
first = true | ||
for (key, value) in scope.values | ||
if first | ||
first = false | ||
else | ||
print(io, ", ") | ||
end | ||
print(io, typeof(key), "@") | ||
show(io, Base.objectid(key)) | ||
print(io, " => ") | ||
show(IOContext(io, :typeinfo => eltype(key)), value) | ||
end | ||
print(io, ")") | ||
end | ||
vchuravy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
struct NoValue end | ||
const novalue = NoValue() | ||
|
||
""" | ||
get(val::ScopedValue{T})::Union{Nothing, Some{T}} | ||
|
||
If the scoped value isn't set and doesn't have a default value, | ||
return `nothing`. Otherwise returns `Some{T}` with the current | ||
value. | ||
""" | ||
function get(val::ScopedValue{T}) where {T} | ||
# Inline current_scope to avoid doing the type assertion twice. | ||
scope = current_task().scope | ||
if scope === nothing | ||
isassigned(val) && return Some(val.default) | ||
return nothing | ||
end | ||
scope = scope::Scope | ||
if isassigned(val) | ||
return Some(Base.get(scope.values, val, val.default)::T) | ||
else | ||
v = Base.get(scope.values, val, novalue) | ||
v === novalue || return Some(v::T) | ||
end | ||
return nothing | ||
end | ||
|
||
function Base.getindex(val::ScopedValue{T})::T where T | ||
maybe = get(val) | ||
maybe === nothing && throw(KeyError(val)) | ||
return something(maybe)::T | ||
end | ||
|
||
function Base.show(io::IO, val::ScopedValue) | ||
print(io, ScopedValue) | ||
vchuravy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print(io, '{', eltype(val), '}') | ||
print(io, '(') | ||
v = get(val) | ||
if v === nothing | ||
print(io, "undefined") | ||
else | ||
show(IOContext(io, :typeinfo => eltype(val)), something(v)) | ||
end | ||
print(io, ')') | ||
end | ||
|
||
""" | ||
with(f, (var::ScopedValue{T} => val::T)...) | ||
|
||
Execute `f` in a new scope with `var` set to `val`. | ||
""" | ||
function with(f, pair::Pair{<:ScopedValue}, rest::Pair{<:ScopedValue}...) | ||
@nospecialize | ||
ct = Base.current_task() | ||
vchuravy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
current_scope = ct.scope::Union{Nothing, Scope} | ||
ct.scope = Scope(current_scope, pair, rest...) | ||
try | ||
return f() | ||
finally | ||
ct.scope = current_scope | ||
end | ||
end | ||
|
||
with(@nospecialize(f)) = f() | ||
|
||
""" | ||
@with vars... expr | ||
|
||
Macro version of `with(f, vars...)` but with `expr` instead of `f` function. | ||
This is similar to using [`with`](@ref) with a `do` block, but avoids creating | ||
a closure. | ||
""" | ||
macro with(exprs...) | ||
if length(exprs) > 1 | ||
ex = last(exprs) | ||
exprs = exprs[1:end-1] | ||
elseif length(exprs) == 1 | ||
ex = only(exprs) | ||
exprs = () | ||
else | ||
error("@with expects at least one argument") | ||
end | ||
for expr in exprs | ||
if expr.head !== :call || first(expr.args) !== :(=>) | ||
error("@with expects arguments of the form `A => 2` got $expr") | ||
end | ||
end | ||
exprs = map(esc, exprs) | ||
quote | ||
ct = $(Base.current_task)() | ||
current_scope = ct.scope::$(Union{Nothing, Scope}) | ||
ct.scope = $(Scope)(current_scope, $(exprs...)) | ||
$(Expr(:tryfinally, esc(ex), :(ct.scope = current_scope))) | ||
end | ||
end | ||
|
||
end # module ScopedValues |
Uh oh!
There was an error while loading. Please reload this page.