Difference between function that accepts abstract types and another that uses type parameters

Consider the following code:

function do_work(first::Number,second::Number)  
    println("Working with $(typeof(first)) and $(typeof(second))")
end 

function do_work(first::A,second::B) where {A<:Number, B<:Number}
    println("Working with $A and $B")
end 
  1. Is there any difference between these two methods?
  2. Should I favour the use of the parametric method over the method that accepts the abstract types? I guess it depends on the type of work I want to do, as pointed out in an answer to my previous question, you can express certain relationships that you can’t with multiple dispatch.
2 Likes
  1. No, both are equivalent.
  2. The first version is easier to read in my eyes. But as said in the answer you linked, and you correctly guess, in other circumstances you might need to use a definition more like the second one.
2 Likes

Rule of thumb

@anon60034542, here is the rule of thumb I think you are seeking:

  • If Julia can tell the types of all parameters passed as a function argument, it will auto-compile specialized code for that permutation of types.
    • This specialized code should typically execute relatively fast
    • It doesn’t really matter which notation you use for the function signature.
  • If Julia can not tell what types are in those data structures, then Julia will have to execute run-time checks to execute the desired/required code.
    • It is these types of run-time checks that tend to slow things down in most dynamic languages.

So, as a rule of thumb, you should use whatever function signature is the simplest to read/write.

  • I agree with @heliosdrm: I find the first signature is the easiest to read - but then the print() statement is slightly easier to read in the second function definition.
  • I therefore suggest you use your professional judgment in choosing the how to write your function.

Array/vector ambiguity

A typical case where Julia can’t easily tell what data is passed in on a function call is when you have arrays/vectors of abstract types:

function f(xvec)
   return Float64[x*x for x in xvec]
end

myReal = Real[1,3,4,9]
myFloat64 = Float64[1,3,4,9]

#Executes sub-optimal version of f() since Julia has to check
#element type for the multiplication @ each iteration:
f(myReal)

#Executes more optimal implementation of f() since Julia *knows*
#type of each element involved in the operation:
f(myFloat64)

If you don’t want users to accidentally call sub-optimal versions of f(), you can choose to only define the specialized version:

#This version won't work on Real[] arrays - so users will be forced to make a Float64[].
function f(xvec::Vector{Float64})
   return [x*x for x in xvec] #You don't need to specify Float64[] here - Julia knows what the result will be.
end

struct ambiguity

If one of the parameters is a struct containing a member without a concrete type, that can generate sub-optimal code:

struct MyStruct
   myfield
end

#Will need to do run-time checks to figure out type used to store
#data in "myfield", and call appropriate machine code:
f(s::MyStruct) = 15*s.myfield

x = f(MyStruct(3)) #Needs run-time checks to tell we have an "Int" in "myfield"
x = f(MyStruct(3.0)) #Needs run-time checks to tell we have an "Float64" in "myfield"

On the other hand, if the type is parameterized, Julia will compile a different (optimized) version of f() for that particular (parameterized) struct type:

struct MyStruct2{T}
   myfield::T
end

#No need for run-time checks here. Julia compiles a different version of f()
#for each type T used in MyStruct2{T}:
f(s::MyStruct2) = 15*s.myfield

#No run-time checks in the following calls!!!:
x = f(MyStruct2(3)) #Julia calls specialized code for f(::MyStruct2{Int})!
x = f(MyStruct2(3.0)) #Julia calls specialized code for f(::MyStruct2{Float64})!
5 Likes

On the other hand, if the type is parameterized, Julia will compile a different (optimized) version of f()

Does that include structs that have a abstract type annotation, like this:

struct MyStruct2
   myfield::Number
end

Is their no run time check for the above struct with the abstract type of Number?

No, if the annotated type is abstract - and not parametrized - it cannot be optimized. Actually you can think of unannotated fileds as if they were annotated with the abstract type Any.

4 Likes