The challenges of documenting generic functions

You may be talking about interfaces. They are the right level of abstraction for some APIs, but there is nothing that requires that an API conforms to a particular interface. Nothing requires that all methods conform to a single interface. It may be bad API design, good API design, historical accident, etc.

In particular, collections evolved in Julia very gradually, and a lot of the inconsistencies you see are of historical origin. You will find that the collections and data structures part of the manual enumerates a couple of β€œloosely defined” interfaces, in contrast to the interfaces section I linked above, and eg for findall & friends methods were added as the language evolved, eg findall(c::AbstractChar, s::AbstractString) came with Julia 1.7.

I agree that it is frustrating, but I think that small, piecemeal changes will be received better than large conceptual reorganizations. Eg it would be a worthy goal for a PR to clean up the docs and concepts for a small set of methods that are related.

For example, all methods of findall at the moment seem to be conceptually equivalent to

findall2(needle, haystack) = 
    [key for (key, value) in pairs(haystack) if neddle == value]

with the caveat that the output only matches the sorting when pairs is ordered. Of course the implementations are optimized, but you will find that the only thing that is conceptually needed for it to work is pairs.

But this is just my rational reconstruction, and while it happens to match the default method, it is not documented. So maybe it should be?

A lot of functions in Base could benefit from a little love along these lines. I think that PRs are ideal for that.

But then you are adding a default method to f. This choice might complicate debugging, because code that should have failed now unexpectedly returns 1.

And would this also cause type instability? If all other methods of f return a String, for instance, this might prevent the compiler to infer the correct return type.

Yes! That is the kind of β€œconcepts” I have in mind.

That’s the very reason that I keep saying β€œconvention”. Nothing β€œrequires”, but

  • A convention will β€œencourage” its adoption.
  • In order to indicate the β€œintention” of the API, we can use the terms and ideas in the convention in the description of our APIs. Then, deviation from the β€œintention” will be discouraged.
  • When we write a new function, we will be encouraged to use the concepts in the convention.

So, yes, I’d like to enhance the β€œinterface” document, so that a newcomer will read it and understand it. If the official documentations use the ideas and terms in the interface document, it will become more readable to newcomers. Also, newcomers will starts to use the ideas in their own programs so that their programs will work with the standard library more harmoniously.

As I explained in one of my earlier posts, I have a first-hand experience in the last point. To summarize, I started to write a function with an array in mind, but I happened to end up with writing a function that works with any β€œindexables”. In the process, I asked questions in this forum, but not all people there were aware of these β€œinterfaces” and recommended array-specific solutions.

This is a huge waste of resources. All julia programmers should know that the standard library is written around these β€œinterfaces” and when you write your own function, you should ask yourself β€œDoes this argument has to be an array? Should I use contiguous integers starting from 1 to iterate over it?”

I agree. I’ll think about how to proceed.

1 Like

I think you misunderstood. @Imiq talked about the functionality of having an β€œextended help section”. This has nothing to do with which method is actually documented. You can document each method of your liking with it’s own string which might contain β€œextended help”.

Code Example
"""
   function f
generic help for f
# Extended help
extended help for f
"""
function f end

"""
   function f(x)
specific help for f(Any)
# Extended help
extended help for f(Any)
"""
function f(x) return x end

"""
   function f(x::Int)
specific help for f(Int)
# Extended help
extended help for f(Int)
"""
function f(x::Int) return 2x end

This will show up as:

help?> f
search: f fd for fma fld fld1 fill fdio frexp foldr foldl flush floor float

  function f normal help - generic function
────────────────────────────────────────────────────────────────────────────
  function f(x) specific help for f(Any)
────────────────────────────────────────────────────────────────────────────
  function f(x::Int) specific help for f(Int)
────────────────────────────────────────────────────────────────────────────
Extended help is available with `??`

help> ?f
search: f fd for fma fld fld1 fill fdio frexp foldr foldl flush floor float

  function f normal help - generic function
  Extended help
  ≑≑≑≑≑≑≑≑≑≑≑≑≑≑≑
  more info - generic function
────────────────────────────────────────────────────────────────────────────
  function f(x) specific help for f(Any)
  Extended help
  ≑≑≑≑≑≑≑≑≑≑≑≑≑≑≑
  extended help for f(Any)
────────────────────────────────────────────────────────────────────────────
  function f(x::Int) specific help for f(Int)
  Extended help
  ≑≑≑≑≑≑≑≑≑≑≑≑≑≑≑
  extended help for f(Int)

Also this has not really to do with type instability. If the compiler can infer the correct method to call and it’s output type, that’s stable. There is no need to have the same return type across all possible functions. In fact f(x) = x is perfectly type-stable for all arguments :slight_smile:

2 Likes

Thanks for the explanation, it’s clear now!

1 Like