Scoped Values

Los valores con alcance proporcionan una implementación de alcance dinámico en Julia.

Lexical scoping vs dynamic scoping

Lexical scoping es el comportamiento predeterminado en Julia. Bajo el alcance léxico, el alcance de una variable se determina por la estructura léxica (textual) de un programa. Bajo el alcance dinámico, una variable está vinculada al valor asignado más reciente durante la ejecución del programa.

El estado de un valor con alcance depende de la ruta de ejecución del programa. Esto significa que para un valor con alcance puedes observar múltiples valores diferentes de manera concurrente.

Julia 1.11

Los valores con alcance se introdujeron en Julia 1.11. En Julia 1.8+ hay una implementación compatible disponible en el paquete ScopedValues.jl.

En su forma más simple, puedes crear un ScopedValue con un valor predeterminado y luego usar with o @with para ingresar a un nuevo ámbito dinámico. El nuevo ámbito heredará todos los valores del ámbito padre (y recursivamente de todos los ámbitos exteriores) con el valor de ámbito proporcionado tomando prioridad sobre las definiciones anteriores.

Veamos primero un ejemplo de ámbito léxico. Una declaración let comienza un nuevo ámbito léxico dentro del cual la definición externa de x es oscurecida por su definición interna.

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

En el siguiente ejemplo, dado que Julia utiliza el alcance léxico, la variable x en el cuerpo de f se refiere al x definido en el alcance global, y entrar en un alcance let no cambia el valor que f observa.

x = 1
f() = @show x
let x = 5
    f() # 1
end
f() # 1

Ahora, usando un ScopedValue, podemos utilizar el alcance dinámico.

using Base.ScopedValues

x = ScopedValue(1)
f() = @show x[]
with(x=>5) do
    f() # 5
end
f() # 1

Tenga en cuenta que el valor observado de ScopedValue depende de la ruta de ejecución del programa.

A menudo tiene sentido usar una variable const para apuntar a un valor de ámbito, y puedes establecer el valor de múltiples ScopedValues con una sola llamada a 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 proporciona una versión macro de with. La expresión @with var=>val expr evalúa expr en un nuevo ámbito dinámico con var establecido en val. @with var=>val expr es equivalente a with(var=>val) do expr end. Sin embargo, with requiere un cierre o función sin argumentos, lo que resulta en un marco de llamada adicional. Como ejemplo, considera la siguiente función f:

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

Si deseas ejecutar f en un ámbito dinámico con a establecido en 2, entonces puedes usar with:

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

Sin embargo, esto requiere envolver f en una función sin argumentos. Si deseas evitar el marco de llamada adicional, entonces puedes usar el macro @with:

@with a=>2 f(10)
Note

Los ámbitos dinámicos son heredados por Tasks, en el momento de la creación de la tarea. Los ámbitos dinámicos no se propagan a través de las operaciones de Distributed.jl.

En el ejemplo a continuación, abrimos un nuevo ámbito dinámico antes de lanzar una tarea. La tarea principal y las dos tareas secundarias observan valores independientes del mismo valor de ámbito al mismo tiempo.

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

Los valores con alcance son constantes a lo largo de un alcance, pero puedes almacenar un estado mutable en un valor con alcance. Solo ten en cuenta que las advertencias habituales para las variables globales se aplican en el contexto de la programación concurrente.

Se requiere cuidado al almacenar referencias a un estado mutable en valores con alcance. Es posible que desee unshare mutable state explícitamente al entrar en un nuevo alcance dinámico.

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

En el ejemplo a continuación, utilizamos un valor con alcance para implementar una verificación de permisos en una aplicación web. Después de determinar los permisos de la solicitud, se ingresa a un nuevo alcance dinámico y se establece el valor con alcance LEVEL. Otras partes de la aplicación pueden consultar el valor con alcance y recibirán el valor apropiado. Otras alternativas como el almacenamiento local de tareas y las variables globales no son adecuadas para este tipo de propagación; nuestra única alternativa habría sido pasar un valor a través de toda la cadena de llamadas.

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

Para acceder al valor de un valor con alcance, el valor con alcance en sí mismo debe estar en el (lexical) alcance. Esto significa que, en la mayoría de los casos, es probable que desees usar valores con alcance como constantes globales.

using Base.ScopedValues
const sval = ScopedValue(1)

De hecho, se puede pensar en los valores con alcance como argumentos de función ocultos.

Esto no excluye su uso como no globales.

using Base.ScopedValues
import Base.Threads: @spawn

function main()
    role = ScopedValue(:client)

    function launch()
        #...
        role[]
    end

    @with role => :server @spawn launch()
    launch()
end

Pero podría haber sido más simple simplemente pasar el argumento de la función directamente en estos casos.

Very many ScopedValues

Si te encuentras creando muchos ScopedValue para un módulo dado, puede ser mejor usar una estructura dedicada para contenerlos.

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)

Crea un contenedor que propaga valores a través de ámbitos dinámicos. Usa with para crear y entrar en un nuevo ámbito dinámico.

Los valores solo se pueden establecer al entrar en un nuevo ámbito dinámico, y el valor al que se hace referencia será constante durante la ejecución de un ámbito dinámico.

Los ámbitos dinámicos se propagan a través de tareas.

Ejemplos

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

Los valores escopados se introdujeron en Julia 1.11. En Julia 1.8+ una implementación compatible está disponible desde el paquete ScopedValues.jl.

source
Base.ScopedValues.withFunction
with(f, (var::ScopedValue{T} => val)...)

Ejecuta f en un nuevo ámbito dinámico con var establecido en val. val se convertirá al tipo T.

Véase también: ScopedValues.@with, ScopedValues.ScopedValue, ScopedValues.get.

Ejemplos

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

Versión macro de with. La expresión @with var=>val expr evalúa expr en un nuevo ámbito dinámico con var establecido en val. val se convertirá al tipo T. @with var=>val expr es equivalente a with(var=>val) do expr end, pero @with evita crear un cierre.

Véase también: ScopedValues.with, ScopedValues.ScopedValue, ScopedValues.get.

Ejemplos

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}}

Si el valor de ámbito no está establecido y no tiene un valor predeterminado, devuelve nothing. De lo contrario, devuelve Some{T} con el valor actual.

Véase también: ScopedValues.with, ScopedValues.@with, ScopedValues.ScopedValue.

Ejemplos

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

Scopes utilizan un diccionario persistente. La búsqueda y la inserción son O(log(32, n)), al entrar en un ámbito dinámico se copia una pequeña cantidad de datos y los datos no modificados se comparten entre otros ámbitos.

El objeto Scope en sí mismo no está destinado a los usuarios y puede ser modificado en una futura versión de Julia.

Design inspiration

Este diseño fue fuertemente inspirado por JEPS-429, que a su vez fue inspirado por variables libres de ámbito dinámico en muchos dialectos de Lisp. En particular, Interlisp-D y su estrategia de enlace profundo.

Un diseño previo discutido fue variables de contexto como PEPS-567 e implementado en Julia como ContextVariablesX.jl.