Best practices / Guidelines for generic methods

I wonder if there are any guidelines on the specificity of method arguments.

I’m thinking about the trade-offs of making a type very general (e.g., ::Any (or omitted), ::AbstractArray) vs making a type more or less specific (e.g., ::AbstractArray{Float64} or ::AbstractArray{<:Number}).

These are the trade-offs I understood from the official style guide and the official performance considerations:

  • Pro more concrete types:
    • enables (future) dispatching
    • more ‘confidence’ in type stability (fewer possible input types for which to check type stability)
    • allow user to convert to required format (e.g., conversion to int)
    • serves as documentation
    • helps to ensure program correctness at compile-time.
  • Pro more general types:
    • works for more use-cases
    • works for non-standard types with additional functionality (e.g., numbers with uncertainty, dual numbers, CartesianIndex instead of Int, BitVector instead of Vector{Bool})

Are there any guidelines or rules-of-thumb on this topic?

Coming mainly from C++, I feel a little uneasy when I see completely unconstrained types for complex functions personally. Things like foo(x) = sqrt(x - 22*oneunit(x)) are completely fine, but when the function has more than two or three lines or when x is used as an iterable or as an array, completely unspecified argument types terrify me a little bit.

On the other hand, I’d assume, one should aim to make types as wide as possible, ideally. What are the ‘soft’ reasons to constrain a type besides the typical ‘hard’ api reasons like “I need dispatch”?

As an example, how would I want to constrain a collection of indices?
::AbstractArray{<:Signed} prevents the use with CartesianIndex. An abstract array of a union is a lot of noise to type and read. I guess, I could just use an unconstrained argument and let the compiler figure out, if the argument is indexable or iterable (depending on the usage) and if the argument’s elements can be used as indices themselves. But that would make the function require additional documentation. Also, I’d fear that I’m missing some edge case, for which the function compiles and runs, but does utter nonsense as the argument does not behave like a collection of indices.

Just my personal take, by no means a well tested guidelines

For public functions, I usually start by writing it with constrained, possibly concrete types. It documents what you’re trying to do, makes the scope of implementation smaller. Only once I have something that tests/works, would I consider widening the definition. That might mean deleting the annotation altogether, adding dispatch, or widening to some abstract type or Union.

For a collection of indices - you might be best off with no type annotation? If it’s not iterable, that will raise, and if the eltype of the collection isn’t usable as an index, getindex will raise a somewhat helpful error message.

It’s ultimately a matter of opinion, but my general preference is to use type annotations when either:

  • A method only makes sense for a particular type (e.g. because I’m accessing a particular field of some struct)
  • The method I’m writing is only applicable for some particular subset of the type tree (e.g. I’m writing something that I think will work for all numbers, but I imagine someone else might want to implement it for string-like things later on).
  • It’s not obvious what the format of the input should be (e.g. should it be a scalar? A collection? A dictionary of some kind?)

Otherwise I try to be pretty loose with input types in order to allow the code to be as generic as possible.

Note that you can do something like const IndexLike = Union{AbstractVector{<:Integer}, CartesianInex} to save some typing, if you want. Or you can even use the holy trait pattern to create an extensible API for describing if something is index-like. But I probably wouldn’t bother in this case.

Yup, this kind of duck-typing is pretty common, and it generally works ok. Documentation is definitely helpful in this case.

1 Like