Документация Engee

Значения с ограниченной областью

Страница в процессе перевода.

Значения с ограниченной областью представляют реализацию динамических областей в 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)

Динамические области наследуются задачами 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

Значения с ограниченной областью постоянны во всей области, но в таком значении можно хранить изменяемое состояние. Помните, что в контексте параллельного программирования существуют обычные предостережения, касающиеся глобальных переменных.

Также необходимо соблюдать осторожность при хранении ссылок на изменяемое состояние в значениях с ограниченной областью. Возможно, при добавлении новой динамической области вы захотите явным образом отменить общий доступ к изменяемому состоянию.

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

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.

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
@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
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
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.