diff --git a/base/scopedvalues.jl b/base/scopedvalues.jl index 339ee717d5fd6..6ccd4687c5c65 100644 --- a/base/scopedvalues.jl +++ b/base/scopedvalues.jl @@ -3,6 +3,7 @@ module ScopedValues export ScopedValue, with, @with +public get """ ScopedValue(x) @@ -54,7 +55,22 @@ Base.eltype(::ScopedValue{T}) where {T} = T """ isassigned(val::ScopedValue) -Test whether a ScopedValue has an assigned value. +Test whether a `ScopedValue` has an assigned value. + +See also: [`ScopedValues.with`](@ref), [`ScopedValues.@with`](@ref), [`ScopedValues.get`](@ref). + +# Examples +```jldoctest +julia> using Base.ScopedValues + +julia> a = ScopedValue(1); b = ScopedValue{Int}(); + +julia> isassigned(a) +true + +julia> isassigned(b) +false +``` """ function Base.isassigned(val::ScopedValue) val.has_default && return true @@ -114,6 +130,21 @@ const novalue = NoValue() If the scoped value isn't set and doesn't have a default value, return `nothing`. Otherwise returns `Some{T}` with the current value. + +See also: [`ScopedValues.with`](@ref), [`ScopedValues.@with`](@ref), [`ScopedValues.ScopedValue`](@ref). + +# Examples +```jldoctest +julia> using Base.ScopedValues + +julia> a = ScopedValue(42); b = ScopedValue{Int}(); + +julia> ScopedValues.get(a) +Some(42) + +julia> isnothing(ScopedValues.get(b)) +true +``` """ function get(val::ScopedValue{T}) where {T} scope = Core.current_scope()::Union{Scope, Nothing} @@ -151,11 +182,32 @@ function Base.show(io::IO, val::ScopedValue) end """ - @with vars... expr + @with (var::ScopedValue{T} => val)... expr + +Macro version of `with`. The expression `@with var=>val expr` evaluates `expr` in a +new dynamic scope with `var` set to `val`. `val` will be converted to type `T`. +`@with var=>val expr` is equivalent to `with(var=>val) do expr end`, but `@with` +avoids creating a closure. + +See also: [`ScopedValues.with`](@ref), [`ScopedValues.ScopedValue`](@ref), [`ScopedValues.get`](@ref). + +# Examples +```jldoctest +julia> using Base.ScopedValues -Macro version of `with(f, vars...)` but with `expr` instead of `f` function. -This is similar to using [`with`](@ref) with a `do` block, but avoids creating -a closure. +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 +``` """ macro with(exprs...) if length(exprs) > 1 @@ -172,9 +224,44 @@ macro with(exprs...) end """ - with(f, (var::ScopedValue{T} => val::T)...) + with(f, (var::ScopedValue{T} => val)...) + +Execute `f` in a new dynamic scope with `var` set to `val`. `val` will be converted +to type `T`. + +See also: [`ScopedValues.@with`](@ref), [`ScopedValues.ScopedValue`](@ref), [`ScopedValues.get`](@ref). + +# Examples +```jldoctest +julia> using Base.ScopedValues + +julia> a = ScopedValue(1); -Execute `f` in a new scope with `var` set to `val`. +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 +``` """ function with(f, pair::Pair{<:ScopedValue}, rest::Pair{<:ScopedValue}...) @with(pair, rest..., f()) diff --git a/doc/src/base/scopedvalues.md b/doc/src/base/scopedvalues.md index 8c3340ab9cf11..6ad553429bb1f 100644 --- a/doc/src/base/scopedvalues.md +++ b/doc/src/base/scopedvalues.md @@ -17,18 +17,15 @@ concurrently. Scoped values were introduced in Julia 1.11. In Julia 1.8+ a compatible implementation is available from the package ScopedValues.jl. -In its simplest form you can create a [`Base.ScopedValue`](@ref) with a -default value and then use [`Base.with`](@ref with) or [`Base.@with`](@ref) to -enter a new dynamic scope. +In its simplest form you can create a [`ScopedValue`](@ref Base.ScopedValues.ScopedValue) +with a default value and then use [`with`](@ref Base.ScopedValues.with) or +[`@with`](@ref Base.ScopedValues.@with) to enter a new dynamic scope. The new scope will +inherit all values from the parent scope (and recursively from all outer scopes) with the +provided scoped value taking priority over previous definitions. -The new scope will inherit all values from the parent scope -(and recursively from all outer scopes) with the provided scoped -value taking priority over previous definitions. - -Let's first look at an example of **lexical** scope: - -A `let` statements begins a new lexical scope within which the outer definition -of `x` is shadowed by it's inner definition. +Let's first look at an example of **lexical** scope. A `let` statement begins +a new lexical scope within which the outer definition of `x` is shadowed by +it's inner definition. ```julia x = 1 @@ -38,9 +35,9 @@ end @show x # 1 ``` -Since Julia uses lexical scope the variable `x` is bound within the function `f` -to the global scope and entering a `let` scope does not change the value `f` -observes. +In the following example, since Julia uses lexical scope, the variable `x` in the body +of `f` refers to the `x` defined in the global scope, and entering a `let` scope does +not change the value `f` observes. ```julia x = 1 @@ -64,7 +61,7 @@ end f() # 1 ``` -Not that the observed value of the `ScopedValue` is dependent on the execution +Note that the observed value of the `ScopedValue` is dependent on the execution path of the program. It often makes sense to use a `const` variable to point to a scoped value, @@ -74,34 +71,54 @@ and you can set the value of multiple `ScopedValue`s with one call to `with`. ```julia using Base.ScopedValues -const scoped_val = ScopedValue(1) -const scoped_val2 = ScopedValue(0) - -# Enter a new dynamic scope and set value -@show scoped_val[] # 1 -@show scoped_val2[] # 0 -with(scoped_val => 2) do - @show scoped_val[] # 2 - @show scoped_val2[] # 0 - with(scoped_val => 3, scoped_val2 => 5) do - @show scoped_val[] # 3 - @show scoped_val2[] # 5 +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 - @show scoped_val[] # 2 - @show scoped_val2[] # 0 + f() # a[] = 3 + g() # b[] = 2 end -@show scoped_val[] # 1 -@show scoped_val2[] # 0 + +f() # a[] = 1 +g() # b[] = 2 ``` -Since `with` requires a closure or a function and creates another call-frame, -it can sometimes be beneficial to use the macro form. +`ScopedValues` provides a macro version of `with`. The expression `@with var=>val expr` +evaluates `expr` in a new dynamic scope with `var` set to `val`. `@with var=>val expr` +is equivalent to `with(var=>val) do expr end`. However, `with` requires a zero-argument +closure or function, which results in an extra call-frame. As an example, consider the +following function `f`: ```julia using Base.ScopedValues +const a = ScopedValue(1) +f(x) = a[] + x +``` + +If you wish to run `f` in a dynamic scope with `a` set to `2`, then you can use `with`: -const STATE = ScopedValue{State}() -with_state(f, state::State) = @with(STATE => state, f()) +```julia +with(() -> f(10), a=>2) +``` + +However, this requires wrapping `f` in a zero-argument function. If you wish to avoid +the extra call-frame, then you can use the `@with` macro: + +```julia +@with a=>2 f(10) ``` !!! note @@ -265,11 +282,11 @@ Base.@kwdef struct Configuration verbose::Bool = false end -const CONFIG = ScopedValue(Configuration()) +const CONFIG = ScopedValue(Configuration(color=true)) -@with CONFIG => Configuration(CONFIG[], color=true) begin +@with CONFIG => Configuration(color=CONFIG[].color, verbose=true) begin @show CONFIG[].color # true - @show CONFIG[].verbose # false + @show CONFIG[].verbose # true end ``` diff --git a/test/scopedvalues.jl b/test/scopedvalues.jl index ca700521b50cd..3af6a3f065c8b 100644 --- a/test/scopedvalues.jl +++ b/test/scopedvalues.jl @@ -52,6 +52,16 @@ emptyf() = nothing @testset "conversion" begin with(emptyf, sval_float=>2) @test_throws MethodError with(emptyf, sval_float=>"hello") + a = ScopedValue(1) + with(a => 2.0) do + @test a[] == 2 + @test a[] isa Int + end + a = ScopedValue(1.0) + with(a => 2) do + @test a[] == 2.0 + @test a[] isa Float64 + end end import Base.Threads: @spawn