Alternatives to `Base.promote_op(op, ::Type...)` for `op::Type`?

In the package I am contributing to, I would like to use Base.promote_op to infer the return type of an arbitrary function given by users. For example, if I want to know the return type of sin(x) for x::Int64, I can do

julia> Base.promote_op(sin, Int64)
Float64

However, if the function’s name is a type name, I get a deprecation warning:

julia> VERSION
v"0.6.1-pre.0"

julia> Base.promote_type(Float64, Int64)
WARNING: promote_op(op::Type, ::Type...) is deprecated as it is no longer needed in Base. If you need its functionality, consider defining it locally.
Stacktrace:
 [1] depwarn(::String, ::Symbol) at ./deprecated.jl:70
 [2] promote_op(::Type{T} where T, ::Type{T} where T) at ./deprecated.jl:399
 [3] eval(::Module, ::Any) at ./boot.jl:235
 [4] eval_user_input(::Any, ::Base.REPL.REPLBackend) at ./REPL.jl:66
 [5] macro expansion at ./REPL.jl:97 [inlined]
 [6] (::Base.REPL.##1#2{Base.REPL.REPLBackend})() at ./event.jl:73
while loading no file, in expression starting on line 0
Float64

Here, Float64 is used as a function (like sin in the earlier example) rather than a type, because in many cases type names double as converter functions. In other words, the intent of the of the above code is to know the return type of the function Float64(x) for x::Int64. Still, Julia 0.6 deprecates such usage of promote_op.

Then, what are the alternatives to promote_op that works for any functions, including the ones whose names are type names? How is this situation handled in Base? I tried to find examples in Base and also searched through issues, but had a hard time finding relevant ones.

1 Like

If you are willing to make the assumption that T(x) always yields a T for any type T (seems reasonable), then you can make your own workaround:

julia> my_promote(x, y) = Base.promote_op(x, y)
my_promote (generic function with 1 method)

julia> my_promote(::Type{T}, y) where {T} = T
my_promote (generic function with 2 methods)

julia> my_promote(sin, Int64)
Float64

julia> my_promote(Float64, Int64)
Float64

but I’d be interested to know if Base provides something that makes this workaround unnecessary.

Yes, this is basically what the deprecation warning message suggests doing. I will use this solution if there is no standard solution.

julia> Core.Inference.return_type(Float64, Tuple{Int64})
Float64

I’ve considered Core.Inference.return_type as well, but it does not properly infer the return type when the argument type is abstract:

julia> Core.Inference.return_type(sin, Tuple{Number})
Any

whereas Base.promote_op does the job correctly:

julia> Base.promote_op(sin, Number)
Number

That’s a bit of an odd requirement, though. After all, I can always implement my own type which is <: Number and have sin(::MyType) = "foo". I don’t think we can make any statements at all about the return type of a function over an abstract type.

julia> struct Foo <: Number
       end

julia> Base.sin(::Foo) = "hello"

julia> Base.promote_op(sin, Number)
Number

julia> sin(Foo())
"hello"

Good point… In fact, promote_op is defined as follows:

function promote_op{S}(f, ::Type{S})
    @_inline_meta
    T = _return_type(f, Tuple{_default_type(S)})
    isleaftype(S) && return isleaftype(T) ? T : Any
    return typejoin(S, T)
end

So, Base.promote_op(sin, Number) simply falls into the last line of the definition: typejoin(S, T), which can be misleading as your example showed…

I think I will use Core.Inference.return_type. Thanks, all!

The behavior of promote_op was initially intended for some operations between arrays and scalars and for broadcast, but we’ve gradually moved from relying on it. The general strategy in Base when we want to rely on inference for predicting, for example, the element type of an array is the following:

  1. Use Core.Inference.return_type or related functions to get an idea of the return type R.
  2. If R is concrete, go ahead, use that.
  3. If that is not the case and your array is empty, then also use R.
  4. Otherwise, you get the type of the first element T and construct an array of that type and define a recursive function that iterates and fills the elements of the array as long as each type S is a subtype of the element type of the array. If that is not the case, you convert your array to an array of type typejoin(T, S) and continue from where you left off (calling the recursive function with the new array) until your fill the array.

You can write a similar function to the one in step 4, and avoid using Core.Inference.return_type at all, except that it won’t handle the empty case.

There are other scenarios where you might want inference to help you to determine a type beforehand, e.g., you want to know the type of the field of an object you’re going to construct (Nullables are an example of this), in that case Core.Inference.return_type might be as best as it can get.

NOTE: This is not an advice encouraging anyone using Core.Inference.return_type whenever they want. It’s not exported for a reason. But in the case it’s really needed, people should try an approach similar to the one described above if possible.

4 Likes