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})!