Scoped Values

Scoped values предоставляют реализацию динамического связывания в Julia.

Lexical scoping vs dynamic scoping

Lexical scoping является поведением по умолчанию в Julia. В рамках лексического связывания область видимости переменной определяется лексической (текстовой) структурой программы. В рамках динамического связывания переменная привязывается к последнему присвоенному значению во время выполнения программы.

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

Julia 1.11

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)
Note

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

source
Base.ScopedValues.withFunction
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
source
Base.ScopedValues.@withMacro
@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
source
Base.isassignedMethod
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
source
Base.ScopedValues.getFunction
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
source

Implementation notes and performance

Scope использует постоянный словарь. Поиск и вставка имеют сложность O(log(32, n)), при входе в динамическую область небольшое количество данных копируется, а неизмененные данные разделяются между другими областями.

Объект Scope сам по себе не предназначен для пользователей и может быть изменен в будущих версиях Julia.

Design inspiration

Этот дизайн был сильно вдохновлён JEPS-429, который, в свою очередь, был вдохновлён динамически областью видимости свободных переменных во многих диалектах Lisp. В частности, Interlisp-D и его стратегией глубокого связывания.

Предыдущий обсуждаемый дизайн был контекстными переменными, такими как PEPS-567, и реализован в Julia как ContextVariablesX.jl.