Scoped values provide an implementation of dynamic scoping in Julia.
Lexical scoping is the default behavior in Julia. Under lexical scoping the scope of a variable is determined by the lexical (textual) structure of a program. Under dynamic scoping a variable is bound to the most recent assigned value during the program's execution.
The state of a scoped value is dependent on the execution path of the program. This means that for a scoped value you may observe multiple different values concurrently.
Scoped values were introduced in Julia 1.11. In Julia 1.8+ a compatible implementation is available from the package ScopedValues.jl.
In its simplest form you can create aScopedValue
with a default value and then usewith
or@with
to enter a new dynamic scope. The new scope will inherit all values from the parent scope (and recursively from all outer scopes) with the provided scoped value taking priority over previous definitions.
Let's first look at an example oflexical scope. Alet
statement begins a new lexical scope within which the outer definition ofx
is shadowed by it's inner definition.
x = 1let x = 5 @show x # 5end@show x # 1
In the following example, since Julia uses lexical scope, the variablex
in the body off
refers to thex
defined in the global scope, and entering alet
scope does not change the valuef
observes.
x = 1f() = @show xlet x = 5 f() # 1endf() # 1
Now using aScopedValue
we can usedynamic scoping.
using Base.ScopedValuesx = ScopedValue(1)f() = @show x[]with(x=>5) do f() # 5endf() # 1
Note that the observed value of theScopedValue
is dependent on the execution path of the program.
It often makes sense to use aconst
variable to point to a scoped value, and you can set the value of multipleScopedValue
s with one call towith
.
using Base.ScopedValuesf() = @show a[]g() = @show b[]const a = ScopedValue(1)const b = ScopedValue(2)f() # a[] = 1g() # b[] = 2# Enter a new dynamic scope and set value.with(a => 3) do f() # a[] = 3 g() # b[] = 2 with(a => 4, b => 5) do f() # a[] = 4 g() # b[] = 5 end f() # a[] = 3 g() # b[] = 2endf() # a[] = 1g() # b[] = 2
ScopedValues
provides a macro version ofwith
. The expression@with var=>val expr
evaluatesexpr
in a new dynamic scope withvar
set toval
.@with var=>val expr
is equivalent towith(var=>val) do expr end
. However,with
requires a zero-argument closure or function, which results in an extra call-frame. As an example, consider the following functionf
:
using Base.ScopedValuesconst a = ScopedValue(1)f(x) = a[] + x
If you wish to runf
in a dynamic scope witha
set to2
, then you can usewith
:
with(() -> f(10), a=>2)
However, this requires wrappingf
in a zero-argument function. If you wish to avoid the extra call-frame, then you can use the@with
macro:
@with a=>2 f(10)
Dynamic scopes are inherited byTask
s, at the moment of task creation. Dynamic scopes arenot propagated throughDistributed.jl
operations.
In the example below we open a new dynamic scope before launching a task. The parent task and the two child tasks observe independent values of the same scoped value at the same time.
using Base.ScopedValuesimport Base.Threads: @spawnconst scoped_val = ScopedValue(1)@sync begin with(scoped_val => 2) @spawn @show scoped_val[] # 2 end with(scoped_val => 3) @spawn @show scoped_val[] # 3 end @show scoped_val[] # 1end
Scoped values are constant throughout a scope, but you can store mutable state in a scoped value. Just keep in mind that the usual caveats for global variables apply in the context of concurrent programming.
Care is also required when storing references to mutable state in scoped values. You might want to explicitlyunshare mutable state when entering a new dynamic scope.
using Base.ScopedValuesimport Base.Threads: @spawnconst sval_dict = ScopedValue(Dict())# Example of using a mutable value wrongly@sync begin # `Dict` is not thread-safe the usage below is invalid @spawn (sval_dict[][:a] = 3) @spawn (sval_dict[][:b] = 3)end@sync begin # If we instead pass a unique dictionary to each # task we can access the dictionaries race free. with(sval_dict => Dict()) do @spawn (sval_dict[][:a] = 3) end with(sval_dict => Dict()) do @spawn (sval_dict[][:b] = 3) endend
In the example below we use a scoped value to implement a permission check in a web-application. After determining the permissions of the request, a new dynamic scope is entered and the scoped valueLEVEL
is set. Other parts of the application can query the scoped value and will receive the appropriate value. Other alternatives like task-local storage and global variables are not well suited for this kind of propagation; our only alternative would have been to thread a value through the entire call-chain.
using Base.ScopedValuesconst LEVEL = ScopedValue(:GUEST)function serve(request, response) level = isAdmin(request) ? :ADMIN : :GUEST with(LEVEL => level) do Threads.@spawn handle(request, response) endendfunction open(connection::Database) level = LEVEL[] if level !== :ADMIN error("Access disallowed") end # ... open connectionendfunction handle(request, response) # ... open(Database(#=...=#)) # ...end
using Base.ScopedValuesimport Base.Threads: @spawnconst sval_dict = ScopedValue(Dict())# If you want to add new values to the dict, instead of replacing# it, unshare the values explicitly. In this example we use `merge`# to unshare the state of the dictionary in parent scope.@sync begin with(sval_dict => merge(sval_dict[], Dict(:a => 10))) do @spawn @show sval_dict[][:a] end @spawn sval_dict[][:a] = 3 # Not a race since they are unshared.end
In order to access the value of a scoped value, the scoped value itself has to be in (lexical) scope. This means most often you likely want to use scoped values as constant globals.
using Base.ScopedValuesconst sval = ScopedValue(1)
Indeed one can think of scoped values as hidden function arguments.
This does not preclude their use as non-globals.
using Base.ScopedValuesimport Base.Threads: @spawnfunction main() role = ScopedValue(:client) function launch() #... role[] end @with role => :server @spawn launch() launch()end
But it might have been simpler to just directly pass the function argument in these cases.
If you find yourself creating manyScopedValue
's for one given module, it may be better to use a dedicated struct to hold them.
using Base.ScopedValuesBase.@kwdef struct Configuration color::Bool = false verbose::Bool = falseendconst CONFIG = ScopedValue(Configuration(color=true))@with CONFIG => Configuration(color=CONFIG[].color, verbose=true) begin @show CONFIG[].color # true @show CONFIG[].verbose # trueend
Base.ScopedValues.ScopedValue
—TypeScopedValue(x)
Create a container that propagates values across dynamic scopes. Usewith
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
julia> using Base.ScopedValues;julia> const sval = ScopedValue(1);julia> sval[]1julia> with(sval => 2) do sval[] end2julia> sval[]1
Scoped values were introduced in Julia 1.11. In Julia 1.8+ a compatible implementation is available from the package ScopedValues.jl.
Base.ScopedValues.with
—Functionwith(f, (var::ScopedValue{T} => val)...)
Executef
in a new dynamic scope withvar
set toval
.val
will be converted to typeT
.
See also:ScopedValues.@with
,ScopedValues.ScopedValue
,ScopedValues.get
.
Examples
julia> using Base.ScopedValuesjulia> a = ScopedValue(1);julia> f(x) = a[] + x;julia> f(10)11julia> with(a=>2) do f(10) end12julia> f(10)11julia> b = ScopedValue(2);julia> g(x) = a[] + b[] + x;julia> with(a=>10, b=>20) do g(30) end60julia> with(() -> a[] * b[], a=>3, b=>4)12
Base.ScopedValues.@with
—Macro@with (var::ScopedValue{T} => val)... expr
Macro version ofwith
. The expression@with var=>val expr
evaluatesexpr
in a new dynamic scope withvar
set toval
.val
will be converted to typeT
.@with var=>val expr
is equivalent towith(var=>val) do expr end
, but@with
avoids creating a closure.
See also:ScopedValues.with
,ScopedValues.ScopedValue
,ScopedValues.get
.
Examples
julia> using Base.ScopedValuesjulia> const a = ScopedValue(1);julia> f(x) = a[] + x;julia> @with a=>2 f(10)12julia> @with a=>3 begin x = 100 f(x) end103
Base.isassigned
—Methodisassigned(val::ScopedValue)
Test whether aScopedValue
has an assigned value.
See also:ScopedValues.with
,ScopedValues.@with
,ScopedValues.get
.
Examples
julia> using Base.ScopedValuesjulia> a = ScopedValue(1); b = ScopedValue{Int}();julia> isassigned(a)truejulia> isassigned(b)false
Base.ScopedValues.get
—Functionget(val::ScopedValue{T})::Union{Nothing, Some{T}}
If the scoped value isn't set and doesn't have a default value, returnnothing
. Otherwise returnsSome{T}
with the current value.
See also:ScopedValues.with
,ScopedValues.@with
,ScopedValues.ScopedValue
.
Examples
julia> using Base.ScopedValuesjulia> a = ScopedValue(42); b = ScopedValue{Int}();julia> ScopedValues.get(a)Some(42)julia> isnothing(ScopedValues.get(b))true
Scope
s use a persistent dictionary. Lookup and insertion isO(log(32, n))
, upon dynamic scope entry a small amount of data is copied and the unchanged data is shared among other scopes.
TheScope
object itself is not user-facing and may be changed in a future version of Julia.
This design was heavily inspired byJEPS-429, which in turn was inspired by dynamically scoped free variables in many Lisp dialects. In particular Interlisp-D and its deep binding strategy.
A prior design discussed was context variables alaPEPS-567 and implemented in Julia asContextVariablesX.jl.
Settings
This document was generated withDocumenter.jl version 1.8.0 onWednesday 9 July 2025. Using Julia version 1.11.6.