Skip to content

Commit

Permalink
Make some improvements to the Scoped Values documentation. (#53628)
Browse files Browse the repository at this point in the history
Fixes #53471.

One thing to note, I changed the signature in the `with` docstring from
this:

```julia
with(f, (var::ScopedValue{T} => val::T)...)
```

to this:

```julia
with(f, (var::ScopedValue{T} => val)...)
```

...since the original signature in the docstring was too strict. I also
added this sentence to the docstring:

```julia
`val` will be converted to type `T`.
```

I added a couple tests that verify the conversion behavior.

(cherry picked from commit 7613c69)
  • Loading branch information
CameronBieganek authored and KristofferC committed Mar 15, 2024
1 parent 0087a11 commit 91da115
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 45 deletions.
101 changes: 94 additions & 7 deletions base/scopedvalues.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module ScopedValues

export ScopedValue, with, @with
public get

"""
ScopedValue(x)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand Down
93 changes: 55 additions & 38 deletions doc/src/base/scopedvalues.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
```

Expand Down
10 changes: 10 additions & 0 deletions test/scopedvalues.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 91da115

Please sign in to comment.