scopedvalues.jl
# 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
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}
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
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)
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()
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