Значения с ограниченной областью
Страница в процессе перевода. |
Значения с ограниченной областью представляют реализацию динамических областей в Julia.
!!! note "Lexical scoping vs dynamic scoping" Лексическое определение областей является поведением по умолчанию в Julia. При лексическом определении областей область переменной определяется лексической (текстовой) структурой программы. При динамическом определении областей переменная привязывается к последнему присвоенному значению во время выполнения программы.
Состояние значения с ограниченной областью зависит от пути выполнения программы. Это означает, что для одного значения может существовать несколько разных значений одновременно.
Совместимость: Julia 1.11
Значения с ограниченной областью появились в 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
# Добавляем новую динамическую область и задаем значение.
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)
Динамические области наследуются задачами |
В примере ниже мы открываем новую динамическую область перед запуском задачи. Родительская задача и две дочерние задачи одновременно наблюдают независимые значения одного и того же значения с ограниченной областью.
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
Значения с ограниченной областью постоянны во всей области, но в таком значении можно хранить изменяемое состояние. Помните, что в контексте параллельного программирования существуют обычные предостережения, касающиеся глобальных переменных.
Также необходимо соблюдать осторожность при хранении ссылок на изменяемое состояние в значениях с ограниченной областью. Возможно, при добавлении новой динамической области вы захотите явным образом отменить общий доступ к изменяемому состоянию.
using Base.ScopedValues
import Base.Threads: @spawn
const sval_dict = ScopedValue(Dict())
# Пример неправильного использования изменяемого значения
@sync begin
# `Dict`не является потокобезопасным, приведенное ниже использование является недопустимым
@spawn (sval_dict[][:a] = 3)
@spawn (sval_dict[][:b] = 3)
end
@sync begin
# Если вместо этого мы передадим каждой задаче уникальный словарь,
# то сможем обращаться к словарям без гонок.
with(sval_dict => Dict()) do
@spawn (sval_dict[][:a] = 3)
end
with(sval_dict => Dict()) do
@spawn (sval_dict[][:b] = 3)
end
end
Пример
В приведенном ниже примере значение с ограниченной областью используется для реализации проверки разрешений на доступ в веб-приложении. После определения разрешений запроса добавляется новая динамическая область и задается значение с ограниченной областью 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
# ... открыть подключение
end
function handle(request, response)
# ...
open(Database(#=...=#))
# ...
end
Идиомы
Отмена общего доступа к изменяемому состоянию
using Base.ScopedValues
import Base.Threads: @spawn
const sval_dict = ScopedValue(Dict())
# Если вы хотите добавить новые значения в словарь, а не заменять
# их, явным образом отмените общий доступ к значениям. В этом примере мы используем `merge`
# для отмены общего доступа к состоянию словаря в родительской области.
@sync begin
with(sval_dict => merge(sval_dict[], Dict(:a => 10))) do
@spawn @show sval_dict[][:a]
end
@spawn sval_dict[][:a] = 3 # Гонка отсутствует, поскольку общий доступ отменен.
end
Значения с ограниченной областью как глобальные переменные
Чтобы получить доступ к значению значения с ограниченной областью, само значение с ограниченной областью должно находиться в (лексической) области. Это означает, что чаще всего вы будете использовать значения с ограниченной областью в качестве постоянных глобальных переменных.
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
Но, возможно, в таких случаях было бы проще просто напрямую передать аргумент функции.
Слишком большое количество значений ScopedValue
Если вы создаете много 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
#
Base.ScopedValues.ScopedValue
— Type
ScopedValue(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.11. В Julia версии 1.8 и более поздних совместимая реализация доступна в пакете ScopedValues.jl. |
#
Base.ScopedValues.with
— Function
with(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
— Method
isassigned(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
— Function
get(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
Примечания к реализации и производительность
Scope
используют постоянный словарь. Поиск и вставка выполняются с помощью O(log(32, n))
, при входе в динамическую область копируется небольшой объем данных, а неизмененные данные распределяются между другими областями.
Сам объект Scope
не предназначен для пользователей и может быть изменен в будущей версии Julia.
Идеи для разработки
На эту разработку в значительной степени оказал влияние документ JEPS-429, который, в свою очередь, опирался на свободные переменные в динамической области во многих диалектах Lisp. В частности, в Interlisp-D и его стратегии глубокой привязки.
Ранее обсуждался дизайн контекстных переменных PEPS-567, реализованный в Julia как ContextVariablesX.