From 58634f0318fa915dc532a2dedf572312e445ef89 Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:29:37 -0600 Subject: [PATCH 1/7] Make some improvements to the Scoped Values documentation. --- base/scopedvalues.jl | 100 ++++++++++++++++++++++++++++++++--- doc/src/base/scopedvalues.md | 92 +++++++++++++++++++++----------- test/scopedvalues.jl | 10 ++++ 3 files changed, 163 insertions(+), 39 deletions(-) diff --git a/base/scopedvalues.jl b/base/scopedvalues.jl index 339ee717d5fd6..d3560fe6e8f4f 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,20 @@ 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> 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 +128,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 +180,33 @@ 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`. `@with var=>val expr` is equivalent to +`with(var=>val) do expr end`, but `@with` avoids creating a closure. + +`val` will be converted to type `T`. + +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 +223,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 2991522900e1a..6e91d2d56f61f 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. - -The new scope will inherit all values from the parent scope +In its simplest form you can create a [`ScopedValues.ScopedValue`](@ref) with a +default value and then use [`ScopedValues.with`](@ref) or [`ScopedValues.@with`](@ref) +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. -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,24 +71,29 @@ 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, @@ -104,6 +106,32 @@ const STATE = ScopedValue{State}() with_state(f, state::State) = @with(STATE => state, f()) ``` +`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. If you wish to avoid the +extra call-frame, then you can use the macro form. 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`: + +```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 Dynamic scopes are inherited by [`Task`](@ref)s, at the moment of task creation. Dynamic scopes are **not** propagated through `Distributed.jl` operations. @@ -265,11 +293,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 From 522937c8248de02a4f8a66f3f67be335763b7e69 Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:46:28 -0600 Subject: [PATCH 2/7] Delete obsolete paragraph from Scoped Values manual page. --- doc/src/base/scopedvalues.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/doc/src/base/scopedvalues.md b/doc/src/base/scopedvalues.md index 6e91d2d56f61f..393c5ba814f4e 100644 --- a/doc/src/base/scopedvalues.md +++ b/doc/src/base/scopedvalues.md @@ -96,16 +96,6 @@ 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. - -```julia -using Base.ScopedValues - -const STATE = ScopedValue{State}() -with_state(f, state::State) = @with(STATE => state, f()) -``` - `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 From 589ca0501ee93791cb6750eecaf2074f5b05f83b Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Wed, 6 Mar 2024 21:48:03 -0600 Subject: [PATCH 3/7] Try to fix the `@ref` references in the Scope Values manual page. --- doc/src/base/scopedvalues.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/src/base/scopedvalues.md b/doc/src/base/scopedvalues.md index 393c5ba814f4e..4feec18c7838c 100644 --- a/doc/src/base/scopedvalues.md +++ b/doc/src/base/scopedvalues.md @@ -17,8 +17,8 @@ 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 [`ScopedValues.ScopedValue`](@ref) with a -default value and then use [`ScopedValues.with`](@ref) or [`ScopedValues.@with`](@ref) +In its simplest form you can create a [`ScopedValue`](@ref Base.ScopedValue) with a +default value and then use [`with`](@ref Base.with) or [`@with`](@ref Base.@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. From d44b453d5116364e76948150106abe533a1163a9 Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:54:43 -0600 Subject: [PATCH 4/7] Add `using Base.ScopedValues` to jldoctext for `isassigned`. --- base/scopedvalues.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/base/scopedvalues.jl b/base/scopedvalues.jl index d3560fe6e8f4f..12d9080f6c782 100644 --- a/base/scopedvalues.jl +++ b/base/scopedvalues.jl @@ -61,6 +61,8 @@ See also: [`ScopedValues.with`](@ref), [`ScopedValues.@with`](@ref), [`ScopedVal # Examples ```jldoctest +julia> using Base.ScopedValues + julia> a = ScopedValue(1); b = ScopedValue{Int}(); julia> isassigned(a) From 0e194af5b8273323af1171520673b3db301f165e Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Thu, 7 Mar 2024 07:03:11 -0600 Subject: [PATCH 5/7] Slightly rearrange the `@with` docstring. --- base/scopedvalues.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/base/scopedvalues.jl b/base/scopedvalues.jl index 12d9080f6c782..6ccd4687c5c65 100644 --- a/base/scopedvalues.jl +++ b/base/scopedvalues.jl @@ -185,10 +185,9 @@ end @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`. `@with var=>val expr` is equivalent to -`with(var=>val) do expr end`, but `@with` avoids creating a closure. - -`val` will be converted to type `T`. +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). From 1e65f604424460196343774f61a7dca171173bb4 Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:28:58 -0600 Subject: [PATCH 6/7] Remove redundant sentence from Scoped Values manual page. --- doc/src/base/scopedvalues.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/src/base/scopedvalues.md b/doc/src/base/scopedvalues.md index 4feec18c7838c..7017f765ddfae 100644 --- a/doc/src/base/scopedvalues.md +++ b/doc/src/base/scopedvalues.md @@ -99,9 +99,8 @@ g() # b[] = 2 `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. If you wish to avoid the -extra call-frame, then you can use the macro form. As an example, consider the following -function `f`: +closure or function, which results in an extra call-frame. As an example, consider the +following function `f`: ```julia using Base.ScopedValues From 37ba58cbfe8551aef28a96b58260da677dd43814 Mon Sep 17 00:00:00 2001 From: Cameron Bieganek <8310743+CameronBieganek@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:50:17 -0600 Subject: [PATCH 7/7] Update the usage of `@ref` in the Scoped Values manual page. --- doc/src/base/scopedvalues.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/src/base/scopedvalues.md b/doc/src/base/scopedvalues.md index 7017f765ddfae..05340f8fc46e9 100644 --- a/doc/src/base/scopedvalues.md +++ b/doc/src/base/scopedvalues.md @@ -17,11 +17,11 @@ 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 [`ScopedValue`](@ref Base.ScopedValue) with a -default value and then use [`with`](@ref Base.with) or [`@with`](@ref Base.@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. +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. 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