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

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