Proposal: @docp for private function documentation and doctests

Actually I believe there is a philosophical point to be made as well. I don’t see the purpose in directly testing a private function at all because by its very nature it is not a complete unit of observable behavior within the system, but rather an implementation detail. A private function either is a subset of one or more public functions’ implementation, or it is unreachable and therefore should be removed; this is demonstrated by the fact that the compiler can remove a private function entirely.

If all of your public functions are fully tested then all code paths through your private functions are inherently tested unless some are unreachable. Therefore any test cases you add for a private function directly is duplicated effort (and bad for programmer productivity). If a private function test case fails then either a public functions test should fail along with it resulting in redundant failures, or it means you’ve done some refactoring that broke your private function but did not change the behavior of any public interface in which case all you’re going to do is change your test case to account for your change and move on with your life.

You describe the option of testing everything through the public interface as “integration-like testing”, but I argue that you are simply defining “unit”, as each individual function in a module, to generically. I would define the “unit” in unit testing to be each function within a module that an external observer could check for observational equivalence between code changes (which is each public function). Changes to anything smaller than this cannot have any effect on the system without changing the behavior of the public function, and because of this I don’t see a private doctest any more useful than a test case that checks if your application’s database is MySQL. You can (granted assuming you are a magical wizard) migrate to Postgresql without causing any breakage in the system and yet your test will fail, and any test that can fail without a real breakage has no need to exist because it means at the end of the day you are testing an implementation detail.

I’m in the same boat and trying to talk myself out of trying to convince my startup to rewrite our Node/Python backend to Elixir, but the BEAM is just too amazing and the thought of writing anything that doesn’t run on it just makes me sad now…except maybe Rust…Rust is good too. Also sorry if my rebuttal came off harsh and/or ranty; at the end of the day I’ll always respect a desire to over test more than the opposite, and I admire your enthusiasm for useful documentation.

5 Likes

I can’t possibly explain how much this statement resonates with me. Especially the Rust commentary.

3 Likes

Good points and no offense taken (naturally!)

1 Like

Before unused module attributes issued a warning at compile time, I was an avid user of @docp to “document” my private functions. By “document” I mean provide a nicely formatted block of text for other developers to look at in the code-base and had nothing to do with ExDoc.

Now, I use multiple lines of standard comments… which just feels messy and disconnected to me. It would certainly (to me) feel a lot nicer to use @docp

If a PR were to be accepted to essentially suppress the unused module attribute warning for @docp, documentation would need to be updated to dispel any confusion over “why doesn’t my private function documentation show up?”… because, well, obviously it shouldn’t, but it could be confusing to some people.

So, if it were implemented, it may cause more inconvenience than it brings to the few of us who like the idea of @docp.

All of that being said, I’d be happy to work on a PR if a nice balance could be struck.

By “document” I mean provide a nicely formatted block of text for other developers to look at in the code-base and had nothing to do with ExDoc.

Now, I use multiple lines of standard comments… which just feels messy and disconnected to me. It would certainly (to me) feel a lot nicer to use @docp

This is exactly what I want, also.

My preferred solution would be to remove the warning and just allow me to use @doc on any method I want without complaining. I’m not at all concerned with generating documentation.

I understand the warning may be helpful for people who are generating rendered documentation and wondering why their private functions don’t show up, so my preference would be some way to inhibit warnings.

I wasn’t able to find anything like a -Wno-private-docs flag for elixirc. Would something like that be a reasonable feature? I’d be OK with the @docp path that lukerollans described, but I’d actually prefer to just use @doc and tell the compiler to not bother me.

1 Like

Testing private functions? Not high on my priority list.

Having nicely formatted and linked documentation for private functions? Yes! I’m always getting credo warnings for documenting private functions. The linking is the key point here - If I’ve got a bunch of recursive functions or a multi-stage builder, it’d really be nice to hand a link over to a newly onboarded dev.

1 Like

Private functions are private for a reason - they are little helpers or they hide the implementation. If private function needs to be reuse somewhere else - move it to different helper module and then provide tests and docs.

My practice regarding private functions is instead of commenting them or even documenting is to name them very accurately (longer names are better than shorter in that case).

This is the thing I really like in Erlang - you export public functions and rest are private. We always have to consider what’s really part of our API, what can be reuse and moved somewhere else and what details we would need to hide (and don’t test directly).

@pragdave wrote an article that is related, not talking about the documentation of private functions but about testing:

https://pragdave.me/blog/2017/03/31/tesing-private-functions.html

For documentation, I do not think that @docp is the solution,
but if you think it is you can easily create a library which registers @defp as a module attribute, and which might use it for performing doctests.

I recommend you to do like this

_ = """
My private function comments goes here ...
"""
defp my_private_fun do
end

It’s simple, no warnings, and your private function is documented.
The _ = even distinguishes from other comments.

I can live with this, though I would prefer @doc support for private functions.

3 Likes

Honestly I would like be able to document every function. I have already though about such documentation types:

  1. Public (for clients and 3rd-party developers)
  2. Internal (for highlighting internal design patterns which should not be changed without discussion)
  3. Private (it’s more like point 2.5)

Now in ex_doc there is no way to have some extra documentation only for internal (new) developers like rules for contributing in library/project etc. Everything like that is hidden and available on 3rd-party services like GitHub markdown preview which I think makes no sense.

Public is of course this one which should be used by other libraries/projects. Internal is for all non-private and non-public functions. Finally private is like internal, but for functions which are not available from other modules.

I could imagine a documentation tool written using scenic which have switches for:

/------------------------------ Settings ------------------------------\
|  [ ] Public API (default)                                            |
|  [x] Internal API (for library developers)                           |
|    [x] Private API (enabled by default when internal is selected)    |
\----------------------------------------------------------------------/

Example code:

defmodule Example1 do
  @moduledoc false # shortcut for: @moduledoc internal: true
  
  @doc "…" # internal function, because in internal module
  def sample, do: do_sample()

  @doc "…" # defp adds @doc private: true
  defp do_sample, do: :sample
end

defmodule Example2 do
  @moduledoc "…" # module with public API

  @doc "…"
  @doc internal: true
  def sample, do: do_sample()

  @doc "…" # defp adds @doc private: true
  defp do_sample, do: :sample

  @doc "…"
  def sample2, do: :sample2
end

defmodule Example3 do
  @moduledoc "…"
  @moduledoc internal: true

  @doc "…" # internal function, because in internal module
  def sample, do: do_sample()

  @doc "…" # defp adds @doc private: true
  defp do_sample, do: :sample
end

Example1 # internal documentation
Example1.sample/0 # internal documentation
Example1.do_sample/0 # private documentation
Example2 # public documentation
Example2.sample/0 # internal documentation
Example2.do_sample/0 # private documentation
Example3 # internal documentation
Example3.sample/0 # internal documentation
Example3.do_sample/0 # private documentation

Also for newbie could be a bit confused, because we can set @spec for private functions, but can’t do same for @doc attribute.

Ideally Elixir compiler could emit warning when other application/library is trying to use any module and function which documentation is marked as internal, but I’m not sure if it’s enough easy to implement to make it worth.

Also …

Yes, but in comments markup is useless. Comments for now can’t be fetched by code in any nice way and finally comments can’t be assigned to specific function (like @doc and @spec attributes are). I see comments to describe something in function body which is requested to not touch (or there is no time to rewrite complicated algorithm), but could be hard to read for new members of such project, so only few lines (i.e. not whole function) are commented.

4 Likes

I’ve had a similar proposal for ex_docs a while ago: https://github.com/elixir-lang/ex_doc/issues/892
It’s about using the new 1.7 module metadata to link documentation to various user groups consuming the documentation, like e.g. internal developer vs. user of the library. It’s different to yours in that it doesn’t impose a fixed structure documentation is categorized by.
In my opinion this would also ease the public vs. private discussion, as a function could easily be public, but not be listed to e.g. the users of a library. So it’s no longer a binary decision of “@doc false” or “it’s documented and therefore public api”, but a spectrum of “this function/information should be available to people within x, but not y”.

5 Likes

I would personally love something like:

@doc "Do something with the public interface"
@doc type: :internal, """
  This is added to the above but is only generated with the documentation generator if
  it's set to generate `MIX_DOCS=internal` type generation or something, `type: ...` could
  of course be a list too.
"""

I would so love to be able to generate a default ‘public’ set of docs as well as a development set of docs for people actually developing and working on the project itself rather than just using it, it’s a huge boon for maintenance having actually generated docs instead of just some random comments so you can get proper linkages and more. Even better if you could decorate code chunks like @doc style: :internal, ... to wrap the entire doc chunk specified by this @doc in a div with the specified style tag so we can have custom CSS apply it uniquely (Like I’d have the theme apply ‘development’ parts of documentation with a slightly different different background, probably just a 50% gray with 15% opacity background so it works in both light and dark theme along with using a :before to apply a unique icon to mark it as development only documentation, even better if ex_doc had a default CSS set for some ‘known types’ like :internal or :dev or whatever you want to call them, maybe default the style to whatever type has unless otherwise specified?). :slight_smile:

Of course being able to apply @doc (or @docp but I’d prefer @doc for defp's regardless for consistency of documentation, but whichever) to defp functions would be needed for this too.

14 Likes

As a developer taking on an established product at a new employer, I can’t emphasize enough how valuable something like @docp would be to me right now.

7 Likes

I think this is a neat proposal. I was surprised to find that private functions don’t have a proper way of being documented. I understand that they come with no guarantee, but code is code, and code is for humans, so I think there should still be a way to write document them.

5 Likes

If you want to learn the purpose of some private function chances are you already have the source code opened in your text editor or IDE, so what would be the advantage of something like @docp instead of just a normal # comment?

1 Like

Pretty formatting. Have you seen the documentation that elixir generates? It’s mouth-watering. I want my private documentation to look like that too.

3 Likes

Hello, I’ve read through the email thread from 2016 and this thread - seems like there’s a pretty common theme with the same back-and-forth about generating documentation for (HTML) consumption.

One major development since that thread in 2016 is the existence of language servers. This means that I can bring up documentation usually only available to h calls in my vim editor, vscode, and more.

The HTML version of the code is my second choice. It’s a backup to when my vim program running the coc-nvim plugin is unable to show the very same documentation that I would see online. Viewing that documentation for the module and for a separately documented function is incredibly easy - just position the cursor over a function or a module and hit K to bring a pop-up like this:

It’s beautiful and on my project we currently do not generate HTML docs.

The language server can be aware of the context it operates in, so allowing private documentation to only be visible to functions within the module can be enforced. This is preempted by a curious (and awesome!) fact that the visibility of the documentation really works in this context because you can’t write calls to private functions without being in the module. In other words, the concern that we’re providing private documentation with the pop-up demonstrated in the screenshot wouldn’t be able to run in the first place due to a compilation error like this:

This error coming up in my editor is beautiful. What an amazing language and set up to reveal to me the compiler errors to guide my programming! To compare, I was a Ruby on Rails developer using vim for 10 years before starting Elixir a few months ago and stumbling upon how to use coc-nvim + elixir-ls to create an Elixir-app-aware vim environment.

My use case here is that I’m fixing a bug involving a brand new module I haven’t worked with before. In fact, we’re pairing on this problem together so some of the work is done by my partner offline when we’re not pairing and then I’m trying to integrate his changes into my code. It’s all new code.

When my partner writes new code that calls a function I’m not familiar with and returns values I’m not familiar with, just knowing ahead of time how many items in a tuple that comes back is a game changer. Private functions arities and return values are just as important when referenced from any other function that you, yourself, are writing code directly in.

Let me tell you that I am a liberal user of IEx.pry and IO.inspect when delving into our well tested code: this is enough to get the run-time information about what results I’m getting back, but an even greater advantage would be to let my code editor give me the information I desire directly inlined in the function that is authorized to be aware of the private details of the private function.

To presently achieve the feat of private HTML documentation, we’re presented with two options: change you code to be public so that @doc and @spec works (which is what people were talking about being ‘forced’) or maintain our own fork of code somewhere that abuses sed in the best case or language-level changes in the worst case. Fair enough, might be worth it to generate HTML.

Now consider the language server now: if I want to have the ability to know the arity of a private function from within a module because I keep my code DRY then I can’t just abuse the sed case here. I either have to change my code to public or build and maintain a parallel environment that integrates with elixir-ls and coc-nvim. I’m trying to just use a private function here, not save the world :slight_smile:

I should be able to see the @spec definition if we’re going to hope that private functions are useful at a glance instead of having to abuse the go-to-definition functionality in coc-nvim which jumps your cursor to the function definition and then using CTRL-o to pop your cursor back to the calling function. That is to say, I can still view private documentation through a key combo like gd C-o but I would prefer to have it pop-up near the code I want to use it.

Please consider adding a @docp tag which would allow for language servers to interpret it properly. Thanks for coming to my Ted Talk.

5 Likes

AFAIK @spec does work for private functions (at least as far as Dialyzer is concerned), even though documentation doesn’t.

2 Likes

That makes sense and one of my screenshots does show the spec working even though it’s defined on a now-private function. However, when invoking it from within the function with valid Elixir code, either the language-server or Dialyzer or coc-nvim doesn’t do the right thing and instead shows No documentation available. This means that the default tools and tool settings do not make use of the @spec in the normal, useful case where a programmer wishes to know the @spec of a private function. Or maybe I’m doing it wrong™, of course :slight_smile:

Here’s a video demonstrating not being able to see the @spec hints on a private method, changing it to public, seeing the documentation, changing it back to private, and then again failing to see the @spec hints:

I was ready to give up on @docp… Look what you’ve done!!

Thank you for your thoughtful analysis.

3 Likes