Scoped Values
Scoped values предоставляют реализацию динамического связывания в Julia.
Lexical scoping является поведением по умолчанию в Julia. В рамках лексического связывания область видимости переменной определяется лексической (текстовой) структурой программы. В рамках динамического связывания переменная привязывается к последнему присвоенному значению во время выполнения программы.
Состояние области видимости значения зависит от пути выполнения программы. Это означает, что для значения области видимости вы можете наблюдать несколько различных значений одновременно.
Scoped values были введены в Julia 1.11. В Julia 1.8+ совместимая реализация доступна из пакета ScopedValues.jl.
В его простейшей форме вы можете создать ScopedValue
с значением по умолчанию, а затем использовать with
или @with
для входа в новую динамическую область. Новая область унаследует все значения от родительской области (и рекурсивно от всех внешних областей), при этом предоставленное значение области будет иметь приоритет над предыдущими определениями.
Давайте сначала рассмотрим пример лексического охвата. Оператор let
начинает новый лексический охват, в рамках которого внешнее определение x
затеняется его внутренним определением.
x = 1
let x = 5
@show x # 5
end
@show x # 1
В следующем примере, поскольку Julia использует лексическую область видимости, переменная x
в теле f
ссылается на x
, определённый в глобальной области видимости, и вход в область видимости let
не изменяет значение, которое наблюдает f
.
x = 1
f() = @show x
let x = 5
f() # 1
end
f() # 1
Теперь, используя ScopedValue
, мы можем использовать динамическое связывание.
using Base.ScopedValues
x = ScopedValue(1)
f() = @show x[]
with(x=>5) do
f() # 5
end
f() # 1
Обратите внимание, что наблюдаемое значение ScopedValue
зависит от пути выполнения программы.
Часто имеет смысл использовать переменную const
, чтобы указывать на область видимости значения, и вы можете установить значение нескольких ScopedValue
с помощью одного вызова with
.
using Base.ScopedValues
f() = @show a[]
g() = @show b[]
const a = ScopedValue(1)
const b = ScopedValue(2)
f() # a[] = 1
g() # 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[] = 2
end
f() # a[] = 1
g() # b[] = 2
ScopedValues
предоставляет макрос-версию with
. Выражение @with var=>val expr
оценивает expr
в новом динамическом контексте с var
, установленным в val
. @with var=>val expr
эквивалентно with(var=>val) do expr end
. Однако with
требует замыкания или функции без аргументов, что приводит к дополнительному вызову фрейма. В качестве примера рассмотрим следующую функцию f
:
using Base.ScopedValues
const a = ScopedValue(1)
f(x) = a[] + x
Если вы хотите запустить f
в динамической области видимости с a
, установленным в 2
, вы можете использовать with
:
with(() -> f(10), a=>2)
Однако это требует обернуть f
в функцию без аргументов. Если вы хотите избежать дополнительного вызова, вы можете использовать макрос @with
:
@with a=>2 f(10)
Динамические области наследуются Task
в момент создания задачи. Динамические области не распространяются через операции Distributed.jl
.
В приведенном ниже примере мы открываем новую динамическую область перед запуском задачи. Родительская задача и две дочерние задачи наблюдают за независимыми значениями одного и того же областного значения одновременно.
using Base.ScopedValues
import Base.Threads: @spawn
const 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[] # 1
end
Значения с областью видимости постоянны на протяжении всей области, но вы можете хранить изменяемое состояние в значении с областью видимости. Просто имейте в виду, что обычные предостережения для глобальных переменных применимы в контексте параллельного программирования.
При хранении ссылок на изменяемое состояние в областях видимости также требуется осторожность. Вы можете захотеть явно unshare mutable state при входе в новую динамическую область видимости.
using Base.ScopedValues
import Base.Threads: @spawn
const 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)
end
end
Example
В приведенном ниже примере мы используем область видимости для реализации проверки разрешений в веб-приложении. После определения разрешений запроса вводится новая динамическая область, и устанавливается область видимости LEVEL
. Другие части приложения могут запрашивать значение области видимости и получат соответствующее значение. Другие альтернативы, такие как локальное хранилище задач и глобальные переменные, не подходят для такого рода распространения; нашей единственной альтернативой было бы передавать значение через всю цепочку вызовов.
using Base.ScopedValues
const LEVEL = ScopedValue(:GUEST)
function serve(request, response)
level = isAdmin(request) ? :ADMIN : :GUEST
with(LEVEL => level) do
Threads.@spawn handle(request, response)
end
end
function open(connection::Database)
level = LEVEL[]
if level !== :ADMIN
error("Access disallowed")
end
# ... open connection
end
function handle(request, response)
# ...
open(Database(#=...=#))
# ...
end
Idioms
Unshare mutable state
using Base.ScopedValues
import Base.Threads: @spawn
const 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
Scoped values as globals
Чтобы получить доступ к значению области видимости, само значение области видимости должно находиться в (лексической) области видимости. Это означает, что чаще всего вы, вероятно, захотите использовать значения области видимости в качестве постоянных глобальных переменных.
using Base.ScopedValues
const sval = ScopedValue(1)
Действительно, можно рассматривать области видимости как скрытые аргументы функции.
Это не исключает их использование в качестве не глобальных.
using Base.ScopedValues
import Base.Threads: @spawn
function main()
role = ScopedValue(:client)
function launch()
#...
role[]
end
@with role => :server @spawn launch()
launch()
end
Но в этих случаях, возможно, было бы проще просто передать аргумент функции напрямую.
Very many ScopedValues
Если вы обнаружите, что создаете много ScopedValue
для одного модуля, возможно, будет лучше использовать специальную структуру для их хранения.
using Base.ScopedValues
Base.@kwdef struct Configuration
color::Bool = false
verbose::Bool = false
end
const CONFIG = ScopedValue(Configuration(color=true))
@with CONFIG => Configuration(color=CONFIG[].color, verbose=true) begin
@show CONFIG[].color # true
@show CONFIG[].verbose # true
end
API docs
Base.ScopedValues.ScopedValue
— TypeScopedValue(x)
Создайте контейнер, который распространяет значения через динамические области. Используйте with
, чтобы создать и войти в новую динамическую область.
Значения могут быть установлены только при входе в новую динамическую область, и значение, на которое ссылаются, будет постоянным в течение выполнения динамической области.
Динамические области распространяются через задачи.
Примеры
julia> using Base.ScopedValues;
julia> const sval = ScopedValue(1);
julia> sval[]
1
julia> with(sval => 2) do
sval[]
end
2
julia> sval[]
1
Динамические значения были введены в Julia 1.11. В Julia 1.8+ совместимая реализация доступна из пакета ScopedValues.jl.
Base.ScopedValues.with
— Functionwith(f, (var::ScopedValue{T} => val)...)
Выполните f
в новой динамической области с var
, установленным в val
. val
будет преобразован в тип T
.
Смотрите также: ScopedValues.@with
, ScopedValues.ScopedValue
, ScopedValues.get
.
Примеры
julia> using Base.ScopedValues
julia> a = ScopedValue(1);
julia> f(x) = a[] + x;
julia> f(10)
11
julia> with(a=>2) do
f(10)
end
12
julia> f(10)
11
julia> b = ScopedValue(2);
julia> g(x) = a[] + b[] + x;
julia> with(a=>10, b=>20) do
g(30)
end
60
julia> with(() -> a[] * b[], a=>3, b=>4)
12
Base.ScopedValues.@with
— Macro@with (var::ScopedValue{T} => val)... expr
Макрос версия with
. Выражение @with var=>val expr
вычисляет expr
в новом динамическом контексте, где var
установлено в val
. val
будет преобразовано в тип T
. @with var=>val expr
эквивалентно with(var=>val) do expr end
, но @with
избегает создания замыкания.
Смотрите также: ScopedValues.with
, ScopedValues.ScopedValue
, ScopedValues.get
.
Примеры
julia> using Base.ScopedValues
julia> const a = ScopedValue(1);
julia> f(x) = a[] + x;
julia> @with a=>2 f(10)
12
julia> @with a=>3 begin
x = 100
f(x)
end
103
Base.isassigned
— Methodisassigned(val::ScopedValue)
Проверьте, имеет ли ScopedValue
присвоенное значение.
Смотрите также: ScopedValues.with
, ScopedValues.@with
, ScopedValues.get
.
Примеры
julia> using Base.ScopedValues
julia> a = ScopedValue(1); b = ScopedValue{Int}();
julia> isassigned(a)
true
julia> isassigned(b)
false
Base.ScopedValues.get
— Functionget(val::ScopedValue{T})::Union{Nothing, Some{T}}
Если область значения не установлена и не имеет значения по умолчанию, верните nothing
. В противном случае верните Some{T}
с текущим значением.
Смотрите также: ScopedValues.with
, ScopedValues.@with
, ScopedValues.ScopedValue
.
Примеры
julia> using Base.ScopedValues
julia> a = ScopedValue(42); b = ScopedValue{Int}();
julia> ScopedValues.get(a)
Some(42)
julia> isnothing(ScopedValues.get(b))
true
Implementation notes and performance
Scope
использует постоянный словарь. Поиск и вставка имеют сложность O(log(32, n))
, при входе в динамическую область небольшое количество данных копируется, а неизмененные данные разделяются между другими областями.
Объект Scope
сам по себе не предназначен для пользователей и может быть изменен в будущих версиях Julia.
Design inspiration
Этот дизайн был сильно вдохновлён JEPS-429, который, в свою очередь, был вдохновлён динамически областью видимости свободных переменных во многих диалектах Lisp. В частности, Interlisp-D и его стратегией глубокого связывания.
Предыдущий обсуждаемый дизайн был контекстными переменными, такими как PEPS-567, и реализован в Julia как ContextVariablesX.jl.