Function parametric type promotion

Say I have a function that does a floating point calculations on the two arguments to the function.

As a convenience to users, hower, additional function definitions are defined so that if a user inputs an Integer, or a Rational, it gets covered to float.

For example:

function needfloat(a::T, b::T) where {T<:Real}
    return a+b 
end 

Given that there are 2 arguments as input to the function, if I want to support all combinations of Float, Integer, and Rational arguments, all the combinations of those are quite long:

needlfoat(a::Int, b::Int) = needfloat(float(a), float(b))
needfloat(a::Int, b::AbstractFloat) = needfloat(float(a), b)
needfloat(a::AbstractFloat, b::Int) = needfloat(a, float(b))
needfloat(a::Rational, b::Rational) = needfloat(float(a), float(b))
needfloat(a::Rational, b::Int) = needfloat(float(a), float(b))
needfloat(a::Rational, b::AbstractFloat) = needfloat(float(a), b)
needfloat(a::Int, b::Rational) = needfloat(float(a), float(b))
needfloat(a::AbstractFloat, b::Rational) = needfloat(a, float(b))

My question: is there a more elegant way to do this? That is, doesn’t require defining the promotion of the arguments for every combination of Integer, Rational, and AbstractFloat?

Why would you need all those explicitly? float(1.0) just returns 1.0, so if you have a needfloat(a::AbstractFloat, b::AbstractFloat) and needfloat(a::Real, b::Real) = needfloat(float(a), float(b)), all arguments will be converted to floating point and passed to the specialized version:

julia> needfloat(a::Real, b::Real) = needfloat(float(a), float(b))    
needfloat (generic function with 1 method)                            
                                                                      
julia> needfloat(a::AbstractFloat, b::AbstractFloat) = "float! $a, $b"
needfloat (generic function with 2 methods)                           
                                                                      
julia> needfloat(1,2.0)                                               
"float! 1.0, 2.0"                                                     
                                                                      
julia> needfloat(1//3,2.0)                                            
"float! 0.3333333333333333, 2.0"                                      

Also, julia already has type promotion for numerics:

https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#Promotion

Unless you’re 100% sure that you require floating point inputs though, I’d leave the promotion up to the predefined promotion rules.

Well, maybe that’s what I needed to know.

However, I am seeing some weirdness.

I’m working on this function to compute Owen’s T function. I call it “owent”.

function owent(h::T1,a::T1) where {T1 <: Real}
# many statements 
t = 0.031167227832798003*h + a*0.027426509708356944
return T1(t)
end

However, the above toy function does not exhibit the behavior of the full, actual, function.

From the actual function:

julia> owent(1,2)
ERROR: MethodError: no method matching owent(::Int64, ::Float64)
Closest candidates are:
  owent(::T1, ::T1) where T1<:Real at c:\Users\amgough\Documents\JuliaCode\StatsFuns.jl\src\owent.jl:63
Stacktrace:
 [1] owent(h::Int64, a::Int64)
   @ StatsFuns c:\Users\amgough\Documents\JuliaCode\StatsFuns.jl\src\owent.jl:95
 [2] top-level scope
   @ REPL[38]:1

What? Not method matching owent(::Int64, ::Float64) ???
Since when is “2” a Float64???

but if I do:

julia> owent(0.5,2)
ERROR: MethodError: no method matching owent(::Float64, ::Int64)
Closest candidates are:
  owent(::T1, ::T1) where T1<:Real at c:\Users\amgough\Documents\JuliaCode\StatsFuns.jl\src\owent.jl:63
Stacktrace:
 [1] top-level scope
   @ REPL[42]:1 

Now the “2” is interpreted as an Int64.

The world wonders.

Very much sounds like you have some old function left over in your running environment that does a partial conversion. That’s what happens when you try to manually take care of all cases instead of leaning on dispatch! Try restarting julia and work from a fresh REPL session.

On a seperate note, this function restricts h and a to be of the same type:

julia> function owent(h::T1,a::T1) where {T1 <: Real}           
       # many statements                                        
       t = 0.031167227832798003*h + a*0.027426509708356944      
       return T1(t)                                             
       end                                                      
owent (generic function with 1 method)                          
                                                                
julia> owent(1.0, 2)                                            
ERROR: MethodError: no method matching owent(::Float64, ::Int64)
Closest candidates are:                                         
  owent(::T1, ::T1) where T1<:Real at REPL[1]:1                 
Stacktrace:                                                     
 [1] top-level scope                                            
   @ REPL[2]:1                                                  
                                                                
julia> owent(1, 2)                                              
ERROR: InexactError: Int64(0.08602024724951189)                 
Stacktrace:                                                     
 [1] Int64                                                      
   @ ./float.jl:812 [inlined]                                   
 [2] owent(h::Int64, a::Int64)                                  
   @ Main ./REPL[1]:4                                           
 [3] top-level scope                                            
   @ REPL[3]:1                                                  

You get an InexactError here because 0.08602024724951189 is not losslessly convertible to an Int64 in T1(t).

In a fresh session:

julia> function owent(h::Real, a::Real)                   
       # many statements                                  
       t = 0.031167227832798003*h + a*0.027426509708356944
       return t # removed the unnecessary conversion
       end                                                
owent (generic function with 1 method)                    
                                                          
julia> owent(1, 2)                                        
0.08602024724951189                                       

Type promotion already ensures that h and a are converted to floating point numbers before doing the operation.

Unless you absolutely positively need a type parameter, you don’t have to specify anything more than absolutely necessary in the function signature. Even with both variables typed Real for dispatch (and nothing more!), julia will still specialize the function to each argument type of the arguments actually passed in.