Scoped Values
スコープ付き値は、Juliaにおける動的スコープの実装を提供します。
Lexical scoping はJuliaのデフォルトの動作です。レキシカルスコープの下では、変数のスコープはプログラムのレキシカル(テキスト的)構造によって決まります。ダイナミックスコープの下では、変数はプログラムの実行中に最も最近割り当てられた値にバインドされます。
スコープ付き値の状態は、プログラムの実行パスに依存しています。これは、スコープ付き値に対して同時に複数の異なる値を観察する可能性があることを意味します。
スコープ付き値は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
ScopedValues
はwith
のマクロバージョンを提供します。式@with var=>val expr
は、var
がval
に設定された新しい動的スコープでexpr
を評価します。@with var=>val expr
はwith(var=>val) do expr end
と同等です。しかし、with
はゼロ引数のクロージャまたは関数を必要とし、これが余分なコールフレームを生じさせます。例として、次の関数f
を考えてみましょう:
using Base.ScopedValues
const a = ScopedValue(1)
f(x) = a[] + x
動的スコープで a
を 2
に設定して f
を実行したい場合は、with
を使用できます:
with(() -> f(10), a=>2)
しかし、これには f
を引数なしの関数でラップする必要があります。余分なコールフレームを避けたい場合は、@with
マクロを使用できます:
@with a=>2 f(10)
ダイナミックスコープは、タスク作成の瞬間に 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.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)...)
var
をval
に設定して新しい動的スコープで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
Base.ScopedValues.@with
— Macro@with (var::ScopedValue{T} => val)... expr
with
のマクロ版。式@with var=>val expr
は、var
がval
に設定された新しい動的スコープでexpr
を評価します。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 として実装されました。