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

Allow users to specify the output eltype of map #116

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Observables"
uuid = "510215fc-4207-5dde-b226-833fc4488ee2"
version = "0.5.5"
version = "0.5.6"

[compat]
julia = "1.6"
Expand Down
47 changes: 44 additions & 3 deletions src/Observables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ See also [`Observables.ObservablePair`](@ref).
connect!(o1::AbstractObservable, o2::AbstractObservable) = on(x-> o1[] = x, o2; update=true)

"""
obs = map(f, arg1::AbstractObservable, args...; ignore_equal_values=false)
obs = map(f, arg1::AbstractObservable, args...; ignore_equal_values=false, out_type=:first_eval)

Creates a new observable `obs` which contains the result of `f` applied to values
extracted from `arg1` and `args` (i.e., `f(arg1[], ...)`.
Expand All @@ -564,10 +564,51 @@ julia> obs = Observable([1,2,3]);
julia> map(length, obs)
Observable(3)
```

# Specifying the element type

The element type (eltype) of the new observable `obs` is determined by the `out_type` kwarg. If
`out_type = :first_eval` (default), then it'll be whatever type is returned the first time `f` is
called.

```jldoctest; setup=:(using Observables)
julia> o1 = Observable{Union{Int, Float64}}(1);

julia> eltype(map(x -> x + 1, o1))
Int
```

If `out_type = :infer`, we'll use type inference to determine the eltype:
```jldoctest; setup=:(using Observables; o1 = Observable{Union{Int, Float64}}(1))
julia> eltype(map(x -> x + 1, o1; out_type=:infer))
Union{Int, Float64}
```

If you use the `:infer` option, then the eltype of `obs` should be considered an implementation
detail that cannot be relied upon and may end up returning `Any` depending on opaque compiler
heuristics.

Finally, if `out_type` isa `Type`, then that type is the unconditional `eltype` of `obs`

```jldoctest setup=:(using Observables; o1 = Observable{Union{Int, Float64}}(1))
julia> eltype(map(x -> x + 1, o1; out_type=Real))
Real
```
"""
@inline function Base.map(f::F, arg1::AbstractObservable, args...; ignore_equal_values::Bool=false, priority::Int = 0) where F
@inline function Base.map(f::F, arg1::AbstractObservable, args...; ignore_equal_values::Bool=false, priority::Int = 0, out_type=:first_eval) where F
if out_type === :first_eval
Obs = Observable
elseif out_type === :infer
RT = Core.Compiler.return_type(f, Tuple{eltype(arg1), eltype.(args)...})
Obs = Observable{RT}
elseif out_type isa Type
Obs = Observable{out_type}
else
msg = "Got an invalid input for the out_type keyword argument, expected either a type, `:first_eval`, or `:infer`, got " * repr(out_type)
error(ArgumentError(msg))
end
# note: the @inline prevents de-specialization due to the splatting
obs = Observable(f(arg1[], map(to_value, args)...); ignore_equal_values=ignore_equal_values)
obs = Obs(f(arg1[], map(to_value, args)...); ignore_equal_values=ignore_equal_values)
map!(f, obs, arg1, args...; update=false, priority = priority)
return obs
end
Expand Down
24 changes: 21 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,14 @@ end
obs = Observable(5)
@test string(obs) == "Observable(5)"
f = on(identity, obs)
@test occursin("Observable(5)\n 0 => identity(x) in Base at operators.jl", plain(obs))
@test occursin("Observable(5)\n 0 => identity(x)", plain(obs))
@test string(f) == "ObserverFunction `identity` operating on Observable(5)"
f = on(x->nothing, obs); ln = @__LINE__
str = plain(obs)
@test occursin("Observable(5)", str)
@test occursin("0 => identity(x) in Base at operators.jl", str)
@test occursin(" in Main at $(@__FILE__)", str)
@test occursin("0 => identity(x)", str)
@test occursin(" Main", str)
@test occursin("runtests.jl", str)
Comment on lines -152 to +159
Copy link
Author

Choose a reason for hiding this comment

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

I had to weaken these tests because the printing of this sort of thing has changed on v1.11, and it was causing the tests to fail.


@test string(f) == "ObserverFunction defined at $(@__FILE__):$ln operating on Observable(5)"
obs[] = 7
Expand Down Expand Up @@ -216,6 +217,23 @@ end
r1[] = 4
@test r3[] === 5.0f0


r4 = Observable{Any}(true)
r5 = map(r4; out_type=:infer) do x
x + 1
end
@test r5[] == 2
r4[] = (1.5 + im)
@test r5[] == 2.5 + im

r6 = Observable{Any}(true)
r7 = map(r6; out_type=Number) do x
x + 1
end
@test r7[] == 2
r6[] = (1.5 + im)
@test r7[] == 2.5 + im

# Make sure `precompile` doesn't error
precompile(r1)
end
Expand Down