Scoped Values

توفر القيم المحددة تنفيذًا للتوجيه الديناميكي في جوليا.

Lexical scoping vs dynamic scoping

Lexical scoping هو السلوك الافتراضي في جوليا. تحت النطاق المعجمي، يتم تحديد نطاق المتغير من خلال الهيكل المعجمي (النصي) للبرنامج. تحت النطاق الديناميكي، يتم ربط المتغير بأحدث قيمة تم تعيينها خلال تنفيذ البرنامج.

حالة القيمة المحددة تعتمد على مسار تنفيذ البرنامج. هذا يعني أنه بالنسبة لقيمة محددة قد تلاحظ قيمًا مختلفة متعددة في نفس الوقت.

Julia 1.11

تم تقديم القيم المحددة في جوليا 1.11. في جوليا 1.8+ ، تتوفر تنفيذ متوافق من الحزمة ScopedValues.jl.

في أبسط صوره يمكنك إنشاء ScopedValue مع قيمة افتراضية ثم استخدام with أو @with للدخول في نطاق ديناميكي جديد. سيت inherit النطاق الجديد جميع القيم من النطاق الأب (وبشكل متكرر من جميع النطاقات الخارجية) مع أخذ القيمة المحددة في النطاق الأولوية على التعريفات السابقة.

لننظر أولاً إلى مثال على النطاق المعجمي. تبدأ عبارة let نطاقًا معجميًا جديدًا يتم فيه حجب التعريف الخارجي لـ x بواسطة تعريفه الداخلي.

x = 1
let x = 5
    @show x # 5
end
@show x # 1

في المثال التالي، نظرًا لأن جوليا تستخدم النطاق المعجمي، فإن المتغير 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 للإشارة إلى قيمة محلية، ويمكنك تعيين قيمة لعدة ScopedValues من خلال استدعاء واحد لـ 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 نفسه ليس موجهًا للمستخدم وقد يتغير في إصدار مستقبلي من جوليا.

Design inspiration

تم استلهام هذا التصميم بشكل كبير من JEPS-429، الذي استلهم بدوره من المتغيرات الحرة ذات النطاق الديناميكي في العديد من لهجات Lisp. بشكل خاص Interlisp-D واستراتيجية الربط العميق الخاصة به.

تم مناقشة تصميم سابق كان يتضمن متغيرات السياق على شكل PEPS-567 وتم تنفيذه في جوليا كـ ContextVariablesX.jl.