Using code_typed to generate docstring templates

I came up with the following idea:

function doctemplate(f, args)
    x        = @code_typed f(args...)
    src      = x[1]
    sig      = split(string(src.parent.def), " @")[1]
    args     = string.(src.slotnames)[2:end]
    argtypes = string.(src.slottypes)[2:end]
    rettype  = string(x[2])

    argvec = args .* " :: " .* argtypes .* " : ARG_DESC"
    argstr = join(argvec, "\n")

    docstr = """
        $sig

    METHOD_DESC

    # Arguments
    $argstr

    # Returns
    $rettype
    """

    return docstr
end

function addnums(a, b::Int16)
    res = a + b
    return res
end

docstr = doctemplate(addnums, (10, Int16(3)));
print(docstr)

That produces:

julia> print(docstr)
    addnums(a, b::Int16)

METHOD_DESC

# Arguments
a :: Int64 : ARG_DESC
b :: Int16 : ARG_DESC

# Returns
Int64

As can be seen, this produces a docstring with signature and types filled in.

I imagine a solution like here could be then implemented to insert these docstrings into the source:

Then writing the docs becomes much more pleasurable, imo.

Is there anything odd about doing this? Few people care about auto-docstrings? It is considered bad practice? Am I reinventing the wheel?

Thanks for any feedback.

Unfortunately Julia’s flexibility and type system is going to make it a little more complicated than that.
Example, your function doesn’t restrict a to be an Int64 but the docstring implies that. That happened because that was the type of the argument you actually passed (ie the 10).

2 Likes

I’ve been actually wondering if a more advanced tool that tries to heuristically infer arguments types from lots of example code (or test suites) for functions that have their arguments untyped. Because unfortunately there are many useful functions that don’t have their arguments typed, by do not in fact accept any type of variable.

The idea in my mind was about a documentation search tool that is more helpful than functions such as methodswith.

Anyway I’m rambling on a tangent. Don’t wory if it doesn’t make sense, that’s probably because it is not very clear in my head yet. I want to flush this idea out and ask the community like you if that might be a useful tool.

1 Like

Instead of allowed types, the docstring demonstrates tested types.

Ie, as used within the package being documented, a is always Int64. There is a guarantee this has been tested.

Other types could still work, but similar ones probably result in degraded performance. Eg, something like String15 vs String or Vector vs MVector. Or it may error, the docstring makes no claim about that.

Is it a bad idea for docstring to only indicate how the function is used within the package itself?

Yes, except its not a heuristic. It is only guaranteed the documented type works.

If that function is intended to always take a::Int64, then no, but you should restrict the annotation accordingly. If you intend users to be able to provide other types for a, then yes it’s very bad to document only one possible type for it because documentation is for usage.

Some functions are documented with abstract types e.g. AbstractArray, but obviously not every concrete subtype that ever existed can be tested at once. The contract in composability is that the developer has adequately implemented and tested an interface and the users in turn have faithfully extended that interface to new types, therefore new code that developers can’t possibly anticipate will still work. Of course some part of that can go wrong in practice, hence bug reporting and patches.

1 Like

Check out this talk by one of the developers from the core team. It was enlightening the first time I watched it.
The idea of only documenting tested types might be very robust in the sense that it’ll never document something that doesn’t work, but it defies one of the most fundamental paradigms in Julia, Multiple Dispatch

As a brief example, I might want to write a function that prints a matrix in a fancy way (maybe formats it as a markdown table). The best practice in Julia is to define fancy_print(m::AbstractMatrix) and use m within the function in a way that is compatible with any matrix object that is a sub-type of AbstractMatrix. If I do this right, then anyone can go ahead and use it as follows.

import LinearAlgebra: UpperTriangular
mat = [1 4 7; 2 5 8; 3 6 9]
upper_tri_mat = UpperTriangular(mat)

fancy_print(mat) # would work
fancy_print(upper_tri_mat) #would work

If appropriate, julia could even produce different machine code for the two different invocations (this is called method specialization)
I might try to test my function with a few matrix types, but I can’t really test with all of them. One of the most elegant things about Julia is that a it allows someone else to define their own matrix type and use it with my fancy_print function, and it’d work right out of the box.

Reminder that you can’t have variables of type AbstractMatrix directly. All variables are of concrete types. Abstract types only define a hierarchy of types (and by convention an implicit common “interface”)


Yeah forgive me about that heuristic thing, it is a very different idea not about documentation at all.

1 Like

Thanks all.

First, I will say my current project isn’t really optimized for multiple dispatch (ie, reusability). Maybe there is a tradeoff vs performance or code-readability there, but I can definitely see a benefit even for my own potential similar future projects.

Second, what about docs for non-exported functions? From looking at julia packages I see it is common for those to have little/no documentation.

Wouldn’t it be better if those had some kind of auto-docstring, then the developer would only need to fill in the placeholder text? The default now is to just let the code speak for itself.

Seems like a “don’t let perfect be the enemy of good” situation.

Edit:
A third point is the expected primary audience for my code is c++, etc devs with no familiarity with julia. So it seems useful to document everything.