Should functions only have one docstring per arity?

This question is a bit philosophical, but it’s probably worth thinking about. Given that we strive for generic functions in Julia, it seems that functions should only have one docstring per arity, each of which describes the meaning of the function for a particular arity. In other words, foo(x, y) should only have one meaning, regardless of the particular types provided as the first and second arguments.

Here are a few Base functions that have more than one docstring per arity:

  • map
  • filter
  • length
3 Likes

Of course not (at least not always). If dispatch is good for Julia code, why wouldn’t it be good for Julia documentation?

Suppose there’s a function f, which may be called with a number of arguments, where the first argument is dispatched on (it could be some kind of option argument). IMO, it can be nice to decompose the doc entries so that different types of the first argument get separate doc strings. But it depends on the situation.

Meta: I think this topic would fit into General usage better than into Internals & Design.

1 Like

Ideally yes. Changing the contract based on the type of the arguments should be avoided. Otherwise we’re not doing generic programming but just overloading functions which is much more complicated.

As Jeff writes,

With multiple dispatch, just as the meaning of a function is important to get consistent, it’s also important to get the meanings of the argument slots consistent when possible. It’s not ideal for a function to treat an argument in a fundamentally different way based on its type: e.g. β€œif this is a Function, then call it on pairs of values, otherwise assume it’s a container providing the values to use”.

I think in 2.0 it would be worth identifying where these are currently used and find other solutions where possible. There might be an exception for when one of the arguments is a type, since type arguments tend to play a special role, but this needs more analysis.

2 Likes

I think as a practical matter it might be ok to make an exception for constructors. For example, String:

String(v::AbstractVector{UInt8})

Create a new String object using the data buffer from byte vector v. If v is
a Vector{UInt8} it will be truncated to zero length and future modification
of v cannot affect the contents of the resulting string. To avoid truncation
of Vector{UInt8} data, use String(copy(v)); for other AbstractVector types,
String(v) already makes a copy.

When possible, the memory of v will be used without copying when the String
object is created. This is guaranteed to be the case for byte vectors returned
by take! on a writable IOBuffer and by calls to read(io, nb). This allows
zero-copy conversion of I/O data to strings. In other cases, Vector{UInt8}
data may be copied, but v is truncated anyway to guarantee consistent
behavior.

────────────────────────────────────────────────────────────────────────

String(s::AbstractString)

Create a new String from an existing AbstractString.
1 Like

It’s also totally possible to satisfy the β€œfully generic” docs while adding valuable additional context that is useful for understanding. For example, the two extra length docstrings 100% satisfy the generic β€œnumber of elements in the collection” for both AbstractArray and String, but each adds extra helpful context (like that it is prod∘size for arrays and number of characters for strings).

7 Likes