diff --git a/Project.toml b/Project.toml index 5abe730..69cc98e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Observables" uuid = "510215fc-4207-5dde-b226-833fc4488ee2" -version = "0.5.5" +version = "0.5.6" [compat] julia = "1.6" diff --git a/src/Observables.jl b/src/Observables.jl index 47af289..4f09146 100644 --- a/src/Observables.jl +++ b/src/Observables.jl @@ -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[], ...)`. @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 75982ac..88d1f37 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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) @test string(f) == "ObserverFunction defined at $(@__FILE__):$ln operating on Observable(5)" obs[] = 7 @@ -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