Proposal: @docp for private function documentation and doctests

proposal
#1

Re: this post by José: https://groups.google.com/d/msg/elixir-lang-talk/0JKd9XKmIas/pDdtzYSQBgAJ

I fully understand and subscribe to the notion of using @doc only for public functions that are supposed to be generating documentation. But I also propose that we “upgrade” the status of private function documentation from its current code-comment style to something more formal and full-featured.

It seems to me that a new @docp would solve the issues of separating public and private documentation; when generating the regular docs we just skip the @docp. This also allows us to have a separate set of private docs generated for in-house use in the future.

And even better (and this is what prompted me to write this proposal in the first placfe), it would also be a great way to test private functions using doctest (or a new doctestp maybe if we want to separate that out), rather than having to make functions that should be private public just to test them.

I have a dream, and it’s to make all documentation formalized, and relegate code comments to what they’re used best for; temporary or semi-permanent blobs of non-documenting but sometimes critical information aimed directly at developers (eg to improve code readability, TODOs, or whatever).

Also: sorry if this topic has been beaten to death already. And a huge thanks to José for this very neat language. Great meeting you at Abstractions.io last fall.

6 Likes
Tests that rely on private methods
Is it necessary to warn about the @doc attribute for a private function
#2

It is worth remembering that a private function does not exist outside of the module that defines it. You cannot test private functions because you can’t invoke a private function outside of the module that defines it.

In fact, the compiler may even remove the private function entirely during compilation. This means a private function only exists when looking at the code and, if you need to look at the code to read it, then it is not documentation.

A private function is, for all purposes, exactly what you defined code comments to be: a temporary or semi-permanent blob which is aimed directly at developers. There is no guarantee it will exist tomorrow, which is why it is private.

Thanks for doing prior research and I’m glad you are enjoying Elixir!

7 Likes
Doctests for private functions
#3

Hey José,

Thanks for an insightful reply. I think I’m on board with the fact that private functions are pretty ephemeral (and as you say can be even inlined or totally eliminated by the compiler). But that wouldn’t necessarily mean (in my mind) that having formalised private docs for private funcs are an invalid idea since they apply to the code, which doesn’t disappear…

And yes, since the private functions are not accessible outside the module, regular testing can’t work. But my thought was that “private doctests” would be a way to do an “inception-like” testing, in some way, to make these often very critical functions be directly testable. And thus avoiding having to go the route of a) using an “integration”-like testing style with multiple functions being tested through the public top-level, as opposed to unit-style granular testing of a single function, b) making them public which defies the intent of the code, or worst c) skipping testing them at all if it’s actually critical that they aren’t public.

I guess I’m missing some technical rather than philosophical point, which makes this idea of “private doctests” impossible or undesirable. From a pure developer productivity and code quality point of view it seems like a no-brainer, I must admit. Perhaps I simply have no brain, then :slight_smile:

PS. To your last line: I’m probably over-enjoying Elixir at this point… Could it be hurtful at some point? :smiley:

2 Likes
#4

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.

4 Likes
#5

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

2 Likes
#6

Good points and no offense taken (naturally!)

1 Like
#7

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.

#8

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.

#9

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.

#10

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).

#11

@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.

#12

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.

1 Like
#13

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.

1 Like
#14

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”.

3 Likes
#15

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.

4 Likes