Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make some improvements to the Scoped Values documentation. #53628

Merged
merged 7 commits into from
Mar 12, 2024
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).
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to #53004 you might need Base.ScopedValues

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Iterators module uses references like [`Iterators.drop`](@ref), so I think this might be ok the way it is.


# 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
90 changes: 54 additions & 36 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.

The new scope will inherit all values from the parent scope
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.

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,55 @@ 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. If you wish to avoid the
extra call-frame, then you can use the macro form. As an example, consider the following
vchuravy marked this conversation as resolved.
Show resolved Hide resolved
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 +283,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