Collapse equivalent function in erlang.org/doc

Hello!

Just before the summer I did an experiment to make the Erlang documentation generated by ExDoc less verbose when there the same function with different arities are documented. In todays docs it looks like this:

io:columns/0 in Erlang/OTP 29 using ExDoc

but it used to be like this:

io:columns/0 in Erlang/OTP 26 using erl_docgen

In Erlang/OTP 26 the two function prototypes have been collapsed into one, though it does not have the information that it is the standard_io device that is the default.

Anyway, so I did an attempt att making the ExDoc version more like the erl_docgen and this is what I came up with:

io:columns/0 in Erlang/OTP 29 using experimental ExDoc.

I’ve collapsed the function signatures into one and use to show what the equivalent function is. The types also collapse if there are duplicates, as you can if you look at io:read/3.

The code uses an heuristic based on equiv doc metadata and looking at the specs to see what should be joined or not and falls back to the original ExDoc variant if the heuristic fails. It also has a heuristic for when it should have the types inline in the signature, or drop it out into a “Types” section in the docs, as can be seen in lists:enumerate/1.

I’ve generated the Erlang/OTP 29.0-rc2 docs with the experimental feature so you can click about and have a look at your own favorite functions.

What do you think? Is it better, worse, the same? Are there things you would have done differently?

7 Likes

I think its better, i often need to go jump between the code and the docs and this new view is definitely clearer.

I definitely think it is a better way of getting the alternatives together.

Nice, but the ≡ is quite hard to spot in there.

I played around with other ways to represent it, but that was the best that I found so far. -> would have been great, but that is used by the type syntax to mean which types are returned, not which function it calls, so I found that to be confusing.

I also considered doing what Elixir does and mark it as a “default”, but as Erlang does not have that syntax it does not really translate to the same thing.

foo(Bar \\ bar :: atom()) -> atom().

Any suggestions about what would make this clearer would be great.

I think having syntax differ from the source code syntax is very important, people wont copy and paste it in into the editor.

The ≡ is readable on my screen, maybe its bad on some others though.

The Erlang/OTP 29 using ExDoc looks best from my perspective because it
is not misleading.

The ≡ is claiming the expressions are identical, but they are not. Each
different function arity can be doing different things with the same
return type. The documentation can be a bit more confusing if you have
the equivalent of function overloading in Erlang, with different
function guards using the same arity. I understand some people may see
the idea of function overloading as dirty, but it can help for
convenience (the complexity needs to be justified).

I don’t think the ≡ needs to be replaced by something else, it just
seems best to not merge the type information like your example to avoid
miscommunication.

The proposal is based on using -doc #{ equiv => foo(bar) }., which is an annotation that means that they are identical. If there is no such annotation, then the the functions are rendered as they are currently. So it is up to the programmer to decide if the functions are identical or not. For example the erlang:adler32/1,2 functions are not merged as they are not marked as equivalent. IIRC there is also a check to see that all functions have the same return type, otherwise it refuses to merge them.

So it will have the exact same information as today, just using less vertical space than it does today.

I’m still unsure whether it actually is an improvement over how functions are rendered today…

I have no issue with the character itself, that’s fine with me. Give or take, any other character would be equally hard to spot. The difficulty I have is telling apart the function and the equivalent function, ie where one ends and where the other starts. I guess it would help if the was in a different color, or if the function and the equivalent were in different colors.

Dunno if that’d be the best or the worst of both worlds but another idea is to indeed use \\ but as convention only, it would be rendered as these two lines:

columns(IoDevice \\ standard_io)
columns(IoDevice :: device()) -> Result

Wouldn’t it be a bit difficult to have it as a convention here but not have it in function definitions as Elixir does? Elixir users would start wondering. And I not interested in having the Elixir way in function definitions.

3 Likes

No, I’m definitely against that. I have a hard time reading that, and can only assume that it means “columns() is equivalent to columns(standard_io)”?

Also, while it may serve the purpose of functions of the same name where certain spots in the lower-arity ones are filled in with defaults for a call to the higher-arity ones, it fails when it comes to equivalent functions with different names (like gb_sets:add(E, S) —> gb_sets:add_element(E, S)), or when arguments are passed on in different formats (like lists:append(L1, L2) —> lists:append([L1, L2]) could be (they are indeed not marked as equivalents in the current doc I know)).

2 Likes

Hmm, couldn’t you use the same => syntax here as well? You would get something like:

columns() => columns(standard_io)
columns(IoDevice :: device()) → Result

(Sorry couldn’t get the red colour :grinning_face: )

Hm, while that => is there because that happens to be map syntax, this really looks pretty nice to me :smiling_cat_with_heart_eyes:

Whoops, I wasn’t thinking about maps there. :face_with_open_eyes_and_hand_over_mouth:

1 Like