Why is it Convention to Repeat Function Signatures in Docstrings

I’m very new to Julia, but the only official guidance on Julia docstring conventions I’ve been able to find suggests repeating the function signature in the docstring. This seems kind of redundant and hard to maintain (if you’re changing function signatures semi-frequently), at least compared to python where the function signature is obtained “by introspection” (though admittedly I’m unsure what this means technically). Just looking for some justification for this convention (if there is any).

2 Likes

You’re right that it is a pain to maintain, but it is hard to question the utility of having the function signature displayed as part of the docstring. It tells the reader how to call the function in a non-ambiguous way.

To help automate this tedious process, you can use

which essentially grabs the signature from the function definition itself.

8 Likes

Due to the paradigm of multiple dispatch, most functions have multiple call signatures, so documenting the signatures is not redundant.

5 Likes

But the docstring is associated to a specific implementation of a function, right? For example if I had

"""
    add(x, y)

adds x and y
"""
function add(x, y)

versus

"""
    add(x::Int, y::Int)

adds two integers x and y
"""
function add(x::Int, y::Int)

The call signature in the docstring is still the same as in the function definition, right?

1 Like

There can be cases where the function is defined with some arguments untyped, but there is still some (more or less) restricted signature that is actually supported in terms of semantic versioning. One example is functions that take any callable object - naively you may want to type that as ::Function, but then you’d exclude callable structs. Of course, Anything is not actually always callable, so you’re only able to point that out in the docstring, not the actual signature.

3 Likes

Yes, the distinction between a function and its methods is a good portion of this story.

A function often has one core meaning but multiple implementations. So you might choose to document how add(x, y) behaves, even if no ::Any, ::Any method exists, because that’s the general behavior new methods should maintain. You can then sometimes want to document methods that have special cases as separate sections, but you can also omit documentation for the more specific methods if they perfectly implement the general behaviors.

8 Likes

This can get very subtle, but both a function (a.k.a. a table of methods), and a method (a specific implementation of the function) have a docstring. The function docstring is usually concatenated from the docstrings of the methods, but it is both common and useful to add a single docstring either to the empty function object or to the first / most general method. That docstrings then serves as the docstring for the entire function and may cover multiple methods. In that context, the function docstring would specify one or more call signatures, which often can’t be derived by introspection.

As an aside, within Documenter, the @docs block allows you to specify whether you want to show a function docstring or a method docstring.

7 Likes

Sometimes the implementation contains a lot of cruft that’s not relevant to the caller, while the docstring’s signature can be written in a more user-friendly way that focuses on how the function/method is called, rather than how it’s implemented. Something like this:

"""
    diffetruct([T,] f, x, args...)

Perform diffetruction...
"""
function diffetruct end

function diffetruct(f, x, args...)
    # simple interface selecting default T
    return diffetruct(default_type(f, x), f, x, args...)
end

@inline function diffetruct(
    ::Type{T}, f::F, x::Integer, args::Vararg{Any,M}
) where {T,F,M}
    # expert interface with custom T
    # extra type parameters to force specialization on type,
    # function, and varargs
    # implementation specific to x::Integer
    [...]
end

@inline function diffetruct(
    ::Type{T}, f::F, x::AbstractFloat, args::Vararg{Any,M}
) where {T,F,M}
    # as above
    # implementation specific to x::AbstractFloat
    [...]
end
3 Likes

I do think we could automatically add the attached method’s signature when no signature is present. That seems like a nice QoL change to me, with no clear downside that I’ve seen yet.

4 Likes

The language server has code actions for adding and update the signatures (see https://github.com/julia-vscode/LanguageServer.jl/pull/1084 and https://github.com/julia-vscode/LanguageServer.jl/pull/1094).

Here is what this looks like in neovim:

If you are using VS Code I think these actions show up as a :bulb: when you hover over the function signature.

10 Likes

A thing that is perhaps worth noting is that traditionally function signatures in julia doc strings are not actually valid julia-code,
but rather follow something like the conventions used by linux man pages.
https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html

In particular

  1. Arguments or option-arguments enclosed in the ‘[’ and ‘]’ notation are optional and can be omitted. Conforming applications shall not include the ‘[’ and ‘]’ symbols in data submitted to the utility.
  2. Arguments separated by the ‘|’ vertical bar notation are mutually-exclusive. Conforming applications shall not include the ‘|’ symbol in data submitted to the utility. Alternatively, mutually-exclusive options and operands may be listed with multiple synopsis lines.
  3. Ellipses ( “…” ) are used to denote that one or more occurrences of an option or operand are allowed. When an option or an operand followed by ellipses is enclosed in brackets, zero or more options or operands can be specified.

More broadly often they will use rough names that indicate the general type of the thing, rather than struct type signatures.

This is particularly relevant when documenting a function not a method.
As it lets you be a bit more expressive about what things are


Personally, for internal function I often omit the signature line.
And often put docstring all on one line
Here the docstring really is just a tiny increment better than a normal comment.

"foos the bar"
foo!(bar) = bar*bar*2
11 Likes

Yes! Why not have the compiler insert the method signature automatically? Shouldn’t this be extremely easy to implement, even at a syntactic level? When you see code like this:

"Some documentation"
function func(x::Whatever{T}, default::Real=0.5; keyword::Integer=5)::SomeType where T<:Real
   # whatever
end

…prepend the current function’s signature to the docstring, so the transformed code will become (internally in the compiler, not literally in the source code):

"""`func(x::Whatever{T}, default::Real=0.5; keyword::Integer=5)::SomeType where T<:Real`

Some documentation
"""
function func(x::Whatever{T}, default::Real=0.5; keyword::Integer=5)::SomeType where T<:Real
   # whatever
end

The compiler likely knows what “function signature” means at this point, so other kind of function definitions like func(x) = 5 or function func(::Vector{T}) where T<:Real should work too.

Julia can output method signatures when there’s no documentation, so these could be used for inserting into docstrings. Unfortunately, currently Julia doesn’t keep default values, types of keyword arguments and return types when printing methods:

julia> function func(a::Real, b::Integer=5, c='f'; kwd::Vector{T}, kwd2::Real=3.141)::Float64 where T<:Real
        5.6789
       end;

help?> func
search: func trunc sinc function run

  No documentation found for private symbol.

  func is a Function.

  # 3 methods for generic function "func" from Main:
   [1] func(a::Real, b::Integer, c; kwd, kwd2)
       @ REPL[1]:1
   [2] func(a::Real, b::Integer; ...)
       @ REPL[1]:1
   [3] func(a::Real; ...)
       @ REPL[1]:1

None of the signatures show that b::Integer, c and kwd2 have default values. kwd and kwd2 look the same in signatures, but 1) they have different types and 2) only kwd2 has a default value, so the printed signature doesn’t tell the full story. Why not?

Does the compiler know that all 3 methods come from the same piece of code? If so, why not put each call signature into each method’s docstring?

When I add a docstring, I get:

julia> begin
        "Docstring"
        function func2(a::Real, b::Integer=5, c='f'; kwd::Vector{T}, kwd3::Real=3.141)::Float64 where T<:Real
         1.2345
        end
       end;

help?> func2
search: func2 func function

  Docstring

…which is plain confusing, because it refuses to tell me how to call the function, even though it can do that, as we saw above. An uninformative docstring remains uninformative.

Whenever a method is documented, the appropriate signature could be automatically included in the documentation. This will also make it impossible to forget to change the signature in the docs after changing the signature in code.

Why not implement this? Does it become too complicated? Is nobody interested in such a feature? Seems like a 100% win-win to me.

1 Like