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

Rework map and map! #62

Merged
merged 1 commit into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions src/Observables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,26 @@ function (mu::MapUpdater)(args...)
end

"""
map!(f, observable::AbstractObservable, args...)
map!(f, observable::AbstractObservable, args...; update::Bool=true)

Updates `observable` with the result of calling `f` with values extracted from args.
`args` may contain any number of `Observable` objects.
`f` will be passed the values contained in the refs as the respective argument.
All other objects in `args` are passed as-is.

By default `observable` gets updated immediately, but this can be suppressed by specifying `update=false`.
"""
function Base.map!(f::F, observable::AbstractObservable, os...) where F
@inline function Base.map!(f::F, observable::AbstractObservable, os...; update::Bool=true) where F
# note: the @inline prevents de-specialization due to the splatting
obsfuncs = onany(MapUpdater(f, observable), os...)
appendinputs!(observable, obsfuncs)
if update
observable[] = f(map(to_value, os)...)
end
return observable
end

function appendinputs!(observable, obsfuncs) # latency: separating this from map! allows dropping the specialization on `f`
if !isdefined(observable, :inputs)
observable.inputs = obsfuncs
else
Expand All @@ -393,9 +404,34 @@ dispatch reasons. `args` may contain any number of `Observable` objects.
`f` will be passed the values contained in the refs as the respective argument.
All other objects in `args` are passed as-is.
"""
function Base.map(f::F, observable::AbstractObservable, os...;
init=f(observable[], map(to_value, os)...)) where F
map!(f, Observable{Any}(init), observable, os...)
@inline function Base.map(f::F, observable::AbstractObservable, os...; kwargs...) where F
# note: the @inline prevents de-specialization due to the splatting
if haskey(kwargs, :init)
Base.depwarn("""
the `init` keyword is deprecated, and the new implementation does not force `Observable{Any}` (it will use the output of `f`).
Instead of `map(f, args...; init=initval)`, use
`map!(f, Observable{T}(initval), args...; update=false)`
To control just the eltype, use
`map!(f, Observable{T}(), args...)`.
""", :map)
dest = Observable{Any}(kwargs[:init])
return map!(f, dest, observable, os...; update=false)
end
map!(f, Observable(f(observable[], map(to_value, os)...)), observable, os...; update=false)
end

"""
map(f, Observable{T}, args...)

Creates an `Observable{T}` containing the result of calling `f` with values extracted from args.
`args` may contain any number of `Observable` objects.
`f` will be passed the values contained in the refs as the respective argument.
All other objects in `args` are passed as-is.
"""
@inline function Base.map(f::F, ::Type{Observable{T}}, os...) where {F, T}
# note: the @inline prevents de-specialization due to the splatting
dest = Observable{T}(f(map(to_value, os)...))
map!(f, dest, os...; update=false)
end

"""
Expand Down
31 changes: 22 additions & 9 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ end
r[] = 3 # shouldn't call test
end

@testset "onany" begin
@testset "onany and map" begin
r = Observable(0)
tested = Ref(false)
onany(1, r) do x, y
Expand All @@ -42,16 +42,26 @@ end
r1 = Observable(0)
r2 = Observable(0)
map!(x->x+1, r2, r1)
@test r2[] == 1
r2 = Observable(0)
map!(x->x+1, r2, r1; update=false)
@test r2[] == 0
r1[] = 1
@test r2[] == 2

r1 = Observable(2)
r2 = map(+, r1, 1)
@test r2[] == 3
r2 = @inferred(map(+, r1, 1))
@test r2[] === 3
@test eltype(r2) === Int
r1[] = 3
@test r2[] == 4

r3 = @inferred(map!(+, Observable{Float32}(), r1, 1))
@test r3[] === 4.0f0
@test eltype(r3) === Float32
r1[] = 4
@test r3[] === 5.0f0

# Make sure `precompile` doesn't error
precompile(r1)
end
Expand Down Expand Up @@ -129,27 +139,22 @@ end
@test c isa Observable
@test c[] == 5
a[] = 100
sleep(0.1)
@test c[] == 103

a = Observable(2)
b = Observable(3)
c = Observable(10)
Observables.@map! c &a + &b
sleep(0.1)
@test c[] == 10
@test c[] == 5
a[] = 100
sleep(0.1)
@test c[] == 103

a = Observable(2)
b = Observable(3)
c = Observable(10)
Observables.@on c[] = &a + &b
sleep(0.1)
@test c[] == 10
a[] = 100
sleep(0.1)
@test c[] == 103
end

Expand Down Expand Up @@ -374,3 +379,11 @@ end
obsf3 = on(sqrt, obs)
@test Observables.methodlist(obsf3).mt.name === :sqrt
end

@testset "deprecations" begin
a = Observable(1)
b = @test_deprecated map(sqrt, a; init=13.0)
@test b[] == 13.0
a[] = 4
@test b[] == 2.0
end