Scoped Values
Los valores con alcance proporcionan una implementación de alcance dinámico en Julia.
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.
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 ScopedValue
s 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)
Los ámbitos dinámicos son heredados por Task
s, 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.ScopedValue
— TypeScopedValue(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
Los valores escopados se introdujeron en Julia 1.11. En Julia 1.8+ una implementación compatible está disponible desde el paquete ScopedValues.jl.
Base.ScopedValues.with
— Functionwith(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
Base.ScopedValues.@with
— Macro@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
Base.isassigned
— Methodisassigned(val::ScopedValue)
Prueba si un ScopedValue
tiene un valor asignado.
Ver también: ScopedValues.with
, ScopedValues.@with
, ScopedValues.get
.
Ejemplos
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}}
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
Implementation notes and performance
Scope
s 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.