Scoped Values

スコープ付き値は、Juliaにおける動的スコープの実装を提供します。

Lexical scoping vs dynamic scoping

Lexical 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がレキシカルスコープを使用しているため、fの本体内の変数xはグローバルスコープで定義された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 変数を使用することはしばしば理にかなっており、1回の with 呼び出しで複数の ScopedValue の値を設定することができます。

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

ScopedValueswithのマクロバージョンを提供します。式@with var=>val exprは、varvalに設定された新しい動的スコープでexprを評価します。@with var=>val exprwith(var=>val) do expr endと同等です。しかし、withはゼロ引数のクロージャまたは関数を必要とし、これが余分なコールフレームを生じさせます。例として、次の関数fを考えてみましょう:

using Base.ScopedValues
const a = ScopedValue(1)
f(x) = a[] + x

動的スコープで a2 に設定して f を実行したい場合は、with を使用できます:

with(() -> f(10), a=>2)

しかし、これには f を引数なしの関数でラップする必要があります。余分なコールフレームを避けたい場合は、@with マクロを使用できます:

@with a=>2 f(10)
Note

ダイナミックスコープは、タスク作成の瞬間に Task に引き継がれます。ダイナミックスコープは、Distributed.jl 操作を通じて伝播されません。

以下の例では、タスクを開始する前に新しい動的スコープを開きます。親タスクと2つの子タスクは、同じスコープされた値の独立した値を同時に観察します。

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

varvalに設定して新しい動的スコープでfを実行します。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は、varvalに設定された新しい動的スコープでexprを評価します。valは型Tに変換されます。@with var=>val exprwith(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.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 として実装されました。