ContextVariablesX.jl
ContextVariablesX.jl is heavily inspired by contextvars in Python (see also PEP 567).
Tutorial
Basic usage
Context variables can be used to manage task-local states that are inherited to child tasks. Context variables are created by @contextvar:
@contextvar cvar1 # untyped, without default
@contextvar cvar2 = 1 # typed (Int), with default
@contextvar cvar3::Int # typed, without defaultNote that running above code in REPL will throw an error because this form work only within a package namespace. To play with @contextvar in REPL, you can prefix the variable name with global:
julia> @contextvar x::Any = 1;@contextvar outside a proper package should be used only for interactive exploration, quick scripting, and testing. Using @contextvar outside packages make it impossible to work with serialization-based libraries such as Distributed.
You can be get a context variable with indexing syntax []
julia> x[]
1It's not possible to set a context variable. But it's possible to run code inside a new context with new values bound to the context variables:
julia> with_context(x => 100) do
x[]
end
100Dynamic scoping
with_context can be used to set multiple context variables at once, run a function in this context, and then rollback them to the original state:
julia> @contextvar y = 1;
@contextvar z::Int;julia> function demo1()
@show x[]
@show y[]
@show z[]
end;
julia> with_context(demo1, x => :a, z => 0);
x[] = :a
y[] = 1
z[] = 0Note that with_context(f, x => nothing, ...) clears the value of x, rather than setting the value of x to nothing. Use Some(nothing) to set nothing. Similar caution applies to set_context (see below).
julia> with_context(x => Some(nothing), y => nothing, z => nothing) do
@show x[]
@show y[]
@show get(z)
end;
x[] = nothing
y[] = 1
get(z) = nothingThus,
with_context(x => Some(a), y => Some(b), z => nothing) do
...
endcan be used considered as a dynamically scoped version of
let x′ = a, y′ = b, z′
...
endUse with_context(f, nothing) to create an empty context and rollback the entire context to the state just before calling it.
julia> with_context(y => 100) do
@show y[]
with_context(nothing) do
@show y[]
end
end;
y[] = 100
y[] = 1Snapshot
A handle to the snapshot of the current context can be obtained with snapshot_context. It can be later restored by with_context.
julia> x[]
1
julia> snapshot = snapshot_context();
julia> with_context(x => 100) do
with_context(snapshot) do
@show x[]
end
end;
x[] = 1Concurrent access
The context is inherited to the child task when the task is created. Thus, changes made after @async/@spawn or changes made in other tasks are not observable:
julia> function demo2()
x0 = x[]
with_context(x => x0 + 1) do
(x0, x[])
end
end
julia> with_context(x => 1) do
@sync begin
t1 = @async demo2()
t2 = @async demo2()
result = demo2()
[result, fetch(t1), fetch(t2)]
end
end
3-element Array{Tuple{Int64,Int64},1}:
(1, 2)
(1, 2)
(1, 2)In particular, manipulating context variables using the public API is always data-race-free.
If a context variable holds a mutable value, it is a data-race to mutate the value when other threads are reading it.
@contextvar local x = [1] # mutable value
@sync begin
@spawn begin
value = x[] # not a data-race
push!(value, 2) # data-race
end
@spawn begin
value = x[] # not a data-race
@show last(value) # data-race
end
endNamespace
Consider "packages" and modules with the same variable name:
julia> module PackageA
using ContextVariablesX
@contextvar x = 1
module SubModule
using ContextVariablesX
@contextvar x = 2
end
end;
julia> module PackageB
using ContextVariablesX
@contextvar x = 3
end;These packages define are three distinct context variables PackageA.x, PackageA.SubModule.x, and PackageB.x that can be manipulated independently.
This is simply because @contextvar creates independent variable "instance" in each context. It can be demonstrated easily in the REPL:
julia> PackageA.x
Main.PackageA.x :: ContextVar [668bafe1-c075-48ae-a52d-13543cf06ddb] => 1
julia> PackageA.SubModule.x
Main.PackageA.SubModule.x :: ContextVar [0549256b-1914-4fcd-ac8e-33f377be816e] => 2
julia> PackageB.x
Main.PackageB.x :: ContextVar [ddd3358e-a77f-44c0-be28-e5dde929c6f5] => 3
julia> (PackageA.x[], PackageA.SubModule.x[], PackageB.x[])
(1, 2, 3)
julia> with_context(PackageA.x => 10, PackageA.SubModule.x => 20, PackageB.x => 30) do
display(PackageA.x)
display(PackageA.SubModule.x)
display(PackageB.x)
(PackageA.x[], PackageA.SubModule.x[], PackageB.x[])
end
Main.PackageA.x :: ContextVar [668bafe1-c075-48ae-a52d-13543cf06ddb] => 10
Main.PackageA.SubModule.x :: ContextVar [0549256b-1914-4fcd-ac8e-33f377be816e] => 20
Main.PackageB.x :: ContextVar [ddd3358e-a77f-44c0-be28-e5dde929c6f5] => 30
(10, 20, 30)Reference
ContextVariablesX.ContextVar — TypeContextVar{T}Context variable type. This is the type of the object var created by @contextvar var. This acts as a reference to the value stored in a task-local context. The macro @contextvar is the only public API to construct this object.
It is unspecified if this type is concrete or not. It may be changed to an abstract type and/or include more type parameters in the future.
Base.get — Methodget(var::ContextVar{T}) -> Union{Some{T},Nothing}Return Some(value) if value is assigned to var. Return nothing if unassigned.
Base.getindex — Methodgetindex(var::ContextVar{T}) -> value::TReturn the value assigned to var. Throw a KeyError if unassigned.
ContextVariablesX.snapshot_context — Methodsnapshot_context() -> snapshot::ContextSnapshotGet a snapshot of a context that can be passed to with_context to run a function inside the current context at later time.
ContextVariablesX.with_context — Functionwith_context(f, var1 => value1, var2 => value2, ...)
with_context(f, pairs)Run f in a context with given values set to the context variables. Variables specified in this form are rolled back to the original value when with_context returns. It act like a dynamically scoped let. If nothing is passed as a value, corresponding context variable is cleared; i.e., it is unassigned or takes the default value. Use Some(value) to set value if value can be nothing.
with_context(f, nothing)Run f in a new empty context. All variables are rewind to the original values when with_context returns.
Note that
var2[] = value2
with_context(var1 => value1) do
@show var2[] # shows value2
var3[] = value3
end
@show var3[] # shows value3and
var2[] = value2
with_context(nothing) do
var1[] = value1
@show var2[] # shows default (or throws)
var3[] = value3
end
@show var3[] # does not show value3are not equivalent.
ContextVariablesX.@contextvar — Macro@contextvar var[::T] [= default]Declare a context variable named var. The type constraint ::T and the default value = default are optional. If the default value is given without the type constraint ::T, its type T = typeof(default) is used.
Context variables defined outside a proper package does not work with Distributed.
Examples
Top-level context variables needs to be declared in a package:
module MyPackage
@contextvar cvar1
@contextvar cvar2 = 1
@contextvar cvar3::Int
endContextVariablesX.with_logger — FunctionContextVariablesX.with_logger(f, logger::AbstractLogger)Like Logging.with_logger but properly propagate the context variables.
ContextVariablesX.current_logger — FunctionContextVariablesX.current_logger() -> logger::AbstractLoggerLike Logging.current_logger but unwraps ContextPayloadLogger.