Do type annotations help Julia determine which method to call?

Consider the following methods:

function do_work(first::Number, second::Number)
    println("Calling function where the parameters are numbers.")
end

function do_work(first::Number,second::AbstractFloat)
    println("Calling method where the first parameter is a number, 
             and the second is an AbstractFloat")
end

In the documentation, we can see the hierarchy for Int32 and Float32:

Number > Real > Integer > Signed > Int32
Number > Real > AbstractFloat > Float32
  1. Do type annotations help Julia to determine which method to call?
number_one :: Int32 = 21
number_two :: Int32 = 47
number_float :: Float32 = 1.15

do_work(number_one,number_two)
do_work(number_two,number_float)
  1. In what order does Julia check the arguments being passed, top to bottom, Number > Real... or bottom to top, Int32 > Signed... to determine which method to call?

No

I am not sure if the right question is being posed here. The most specific method will be called. Thus, if the second parameter isa AbstractFloat, the second method will be called.

2 Likes

Not, I suspect, in the way you’re thinking. Values in Julia have types, variables are just labels attached to those values.

With that said, type declarations do have an effect in that they implicitly try to convert the right-hand side to the specified type:

julia> function f()
         x::Float64 = 1
         @show typeof(x)
       end
f (generic function with 1 method)

julia> f()
typeof(x) = Float64

What happened here is that the ::Float64 declaration caused Julia to insert a call to convert(Float64,...) which does actually produce a value of type Float64. You can see this in the @code_lowered output:

julia> @code_lowered f()
CodeInfo(
1 ─ %1 = Base.convert(Main.Float64, 1)
│        x = Core.typeassert(%1, Main.Float64)

So the ::Float64 turned into a call to convert and a call to typeassert. That means that you’ll get the same result here by doing:

julia> function g()
         x = convert(Float64, 1)
         @show typeof(x)
       end
g (generic function with 1 method)

julia> g()
typeof(x) = Float64

or more simply by doing Float64(1) or just 1.0.

So the annotation that you added to a variable here had an effect, but only because the convert call that it inserted actually did change the type of the value being assigned to that variable.

This comes back to the same point here: How does a dictionary store structs? - #2 by rdeits – there is no distinction between “compile-time” and “run-time” types in Julia.

2 Likes

I think I have a misunderstanding of the documentation.

The most specific method definition matching the number and types of the arguments will be executed when the function is applied.

When checking for the type of arguments, I always assumed Julia will start with what it knows:

  • We can’t use length to determine what method to call, because both methods require two arguments.

  • The first method call to do_work() was given an Int32 as the first argument.

None of the methods have Int32 as the first argument, so we move up to Signed, Integer, etc… until it hits the final type, Number. Both methods take Number, so it moves to the second argument, Float32, no method takes a Float32, moving up, we check if any method takes AbstractFloat, which the second method does, so that method is called.

In other words, how does Julia match Float32 with AbstractFloat? Does it move up the hierarchy until it finds the closest match?

1 Like

Internally, there’s a do_work method table that is sorted by specificity, so any do_work dispatch only needs to provide the call signature and performs an O(n) search through the method table. The first method to match (and therefore the most specific) is do_work(::Number, ::AbstractFloat), so Julia simply specializes on that one.
But you’re not entirely wrong! Sorting methods by specificity and matching the call signatures both depend on an efficient subtyping algorithm, which you can peruse yourself if you’re well-versed with C. To juggle with Unions and UnionAlls, invariant parametric types and covariant tuple types, it is of course a lot more complicated than only traversing the type hierarchy, but a part of the process indeed does just that to incorporate subtyping relations declared by the user.

3 Likes