When should one type annotate a function signature?

When developing a package, what is the convention when it comes to type annotating a function’s arguments? For instance, say I have a function that takes in three arguments, like so:

function f(a, b, c)
    @. a * b + c
end

and say that I always intend for a to be a Vector, and b and c to be real numbers. Then, should I type annotate the function signature like so:

function f(a::AbstractVector, b::Real, c::Real)
    @. a + b * c
end

or should I only do so when I intend to write different methods for Vectors and Matrixs, and want to use multiple dispatch to do that? Does type annotating a function’s signature help documentation, or should it be only used for dispatching on a function’s arguments?

3 Likes

With libraries, I would generally lean towards Abstract annotations, or no annotations if nothing sufficiently abstract exists. I wouldn’t typically use a AbstractVector annotation, because a lot of code that works with Vector works with other collections like Set, and there’s no way to annotate general Iterables. But I would use Number, which is pretty general.

For applications, I’ll generally be a lot more specific, because the odds of someone wanting to use the code in a different way are much smaller, and the users of the code are my coworkers so it’s easier to change if needed.

Types are great documentation IMO, and definitely worth using that way in applications. My coworkers and I find it much easier to debug code with consistent annotations.

5 Likes

See also the discussion of argument-type declarations in the Julia manual, which discusses the utility (but not necessity) of declarations for clarity and correctness purposes, in addition to dispatch.

4 Likes

With regard to your question about docs: Documenter.jl doesn’t pick up type annotations as far as I’m aware – just whatever you write in your docstring. That said, of someone calls methods(f) on your function, they will see it printed out with the type annotations.

And if they go to read your source code, it will help them understand the meaning of your arguments. That’s part of the reason I’m working on DuckTypes.jl. I want to be able to annotate arguments with their meaning without being bound to the type lattice.

2 Likes

That is how I have been using them till now, since using anything not sufficiently abstract will overspecialise the function. I am mostly writing libraries for now, hence I was not sure if using abstract annotations like AbstractVector and Real was alright or not. This clears it up a bit though. Thanks!

Thanks for sharing this @stevengj!I was a bit concerned about a prospective performance hit, but it is clearly written that there is no such hit in the docs:

Argument-type declarations normally have no impact on performance: regardless of what argument types (if any) are declared, Julia compiles a specialized version of the function for the actual argument types passed by the caller. For example, calling fib(1) will trigger the compilation of specialized version of fib optimized specifically for Int arguments, which is then re-used if fib(7) or fib(15) are called.