4

In Julia, I most often see code written like fun(n::T) where T<:Integer, when the function works for all subtypes of Integer. But sometimes, I also see fun(n::Integer), which some guides claim is equivalent to the above, whereas others say it's less efficient because Julia doesn't specialize on the specific subtype unless the subtype T is explicitly referred to.

The latter form is obviously more convenient, and I'd like to be able to use that if possible, but are the two forms equivalent? If not, what are the practicaly differences between them?

Sundar R
  • 13,776
  • 6
  • 49
  • 76
  • AFAIK they are equivalent. The recommendations I know are to use `fun(n::Integer)` unless you would need `T` somewhere in function body. This is exactly what https://docs.julialang.org/en/latest/manual/methods/#Defining-Methods-1 recommends in `f(x::Number, y::Number)` example. You can see that if you run both definitions they overwrite each other if you check methods for `fun`. Also you can check that both forms generate exactly the same code with `@code_warntype` or `@code_native` by passing different types of arguments. – Bogumił Kamiński May 02 '18 at 15:18
  • They do seem to generate the same code, and I now see that the code in Julia source uses `T` only when the subtype is used either in the definition or in another part of the signature eg. `(x::T, y::T)`, otherwise they use the supertype directly. It's a somewhat insane idea that Julia's compiler, with all its cleverness, won't bother generating subtype-specific code if only the supertype is mentioned in the signature - I believe it was some Youtube beginner's tutorial that mentioned it, and I'm glad it isn't true. Could you add your comment as an answer please? – Sundar R May 02 '18 at 15:40
  • Just by chance, I found further confirmation of this: [an answer from Jeff Bezanson](https://github.com/JuliaLang/julia/issues/8142#issuecomment-53498588) himself that "those are exactly the same". And these days Julia *is* smart enough to replace the first definition with the second, so `f` has only one method at the end. – Sundar R May 12 '18 at 07:03

2 Answers2

5

Yes Bogumił Kamiński is correct in his comment: f(n::T) where T<:Integer and f(n::Integer) will behave exactly the same, with the exception the the former method will have the name T already defined in its body. Of course, in the latter case you can just explicitly assign T = typeof(n) and it'll be computed at compile time.

There are a few other cases where using a TypeVar like this is crucially important, though, and it's probably worth calling them out:

  • f(::Array{T}) where T<:Integer is indeed very different from f(::Array{Integer}). This is the common parametric invariance gotcha (docs and another SO question about it).
  • f(::Type) will generate just one specialization for all DataTypes. Because types are so important to Julia, the Type type itself is special and allows parameterization like Type{Integer} to allow you to specify just the Integer type. You can use f(::Type{T}) where T<:Integer to require Julia to specialize on the exact type of Type it gets as an argument, allowing Integer or any subtypes thereof.
mbauman
  • 30,958
  • 4
  • 88
  • 123
4

Both definitions are equivalent. Normally you will use fun(n::Integer) form and apply fun(n::T) where T<:Integer only if you need to use specific type T directly in your code. For example consider the following definitions from Base (all following definitions are also from Base) where it has a natural use:

zero(::Type{T}) where {T<:Number} = convert(T,0)

or

(+)(x::T, y::T) where {T<:BitInteger} = add_int(x, y)

And even if you need type information in many cases it is enough to use typeof function. Again an example definition is:

oftype(x, y) = convert(typeof(x), y)

Even if you are using a parametric type you can often avoid using where clause (which is a bit verbose) like in:

median(r::AbstractRange{<:Real}) = mean(r)

because you do not care about the actual value of the parameter in the body of the function.

Now - if you are Julia user like me - the question is how to convince yourself that this works as expected. There are the following methods:

  • you can check that one definition overwrites the other in methods table (i.e. after evaluating both definitions only one method is present for this function);
  • you can check code generated by both functions using @code_typed, @code_warntype, @code_llvm or @code_native etc. and find out that it is the same
  • finally you can benchmark the code for performance using BenchmarkTools

A nice plot explaining what Julia does with your code is here http://slides.com/valentinchuravy/julia-parallelism#/1/1 (I also recommend the whole presentation to any Julia user - it is excellent). And you can see on it that Julia after lowering AST applies type inference step to specialize function call before LLVM codegen step.

You can hint Julia compiler to avoid specialization. This is done using @nospecialize macro on Julia 0.7 (it is only a hint though).

Bogumił Kamiński
  • 66,844
  • 3
  • 80
  • 107