Defdelegate does not allow access to docs

Hi.

I have a module that acts as a façade and only contains defdelegate expressions to functions in other modules (more concrete: it is a Phoenix context in which functions are implemented in internal modules splitted for convenience).

The issue is that when I try to get the documentation to one of the of the functions in the module I don’t get any documentation at all.

Let me give you an example.

This would be the public module, the one meant to be accessed from the outside.

defmodule MyApp.Examples do
  defdelegate get_example(params), to: MyApp.Examples.MyInternalModule
end

This would be the internal module, the one that provides the actual implementation but we don’t want to be exposed as a public API to the outside. It is actually an implementation detail.

defmodule MyApp.Examples.MyInternalModule do
  @doc """
  This documentation is not available in the delegator module.
  """
  def get_example(params), do: #whatever
end

I was expecting that the documentation of MyApp.Examples.get_example/1 would be automatically retrieved from MyApp.Examples.MyInternalModule.get_example/1. But I am getting this:

iex(1)> h MyApp.Examples.get_example

                          def get_example(params)

Instead of the expected:

iex(1)> h MyApp.Examples.MyInternalModule.get_example

                          def get_example(params)

This documentation is not available in the delegator module

I know that I can get the documentation by looking into the internal module directly. But I think that it somewhat breaks the encapsulation from outside callers, since they should not worry about internal implementation details (I may move my function to another internal module without changing the public API).

Is defdelegate working like this by design? If so, would it be also a good idea to delegate the documentation to the original function in the same way as it is done by the actual implementation?

2 Likes

Is defdelegate working like this by design?

I think defdelegate just wraps the function that you delegate it to, so it doesn’t know that the function would have any docs/specs. You might define docs on the defdelegate.

If so, would it be also a good idea to delegate the documentation to the original function in the same way as it is done by the actual implementation?

I would like it. When I used it in a scenario similar to yours, the absence of docs surprised me as well. But defdelegate is probably supposed to be like that, it’s the public interface to some other function, so it’s defdelegate that should have the docs (since it’s public) and not it’s actual implementation. But it would be nice if specs could be transferred somehow, since they belong with the implementation, I think …

1 Like

This is intentional—see the discussion here.

In short you don’t always want the delegate to be publicly documented and often want it to be documented differently than the source.

There is also the problem of the documentation potentially referencing it’s own __MODULE__ namespace—that probably doesn’t belong in your delegate’s documentation if you are trying to wrap a third-party API whose existence is an internal implementation detail.

That isn’t to say there aren’t situations where it makes sense. You can write your own macros that solve these questions with answers tailored to your specific situation, or just maintain duplicated versions of delegate documentation. But generally exactly how defdelegate would need to behave in these situations is very use-case specific and therefore no single answer is common enough to reasonably be a feature of the macro.


I’ve hand rolled my own macros within Mnemonix (though I’m still refactoring the approach a bit) you might be interested in referring to if you go down this path. It lives in Mnemonix.Behaviour and is meant to be used in the (common for that library, less so elsewhere) situation where you want to create a behaviour with default implementations for overridable callbacks. It’s one of those __using__ macros that defines __using__ macros which normally causes me to reconsider my life decisions but it’s justifiable in this scenario.

The gist of it is to inject an @on_definition callback into the source module, store any information you want to capture around each definition like its docs into a module attribute, then have a @before_compile hook store all the collected information somewhere accessible on the delegating module.

Then in the nested __using__ macro you can consume that data to define things like delegate functions when the behaviour module itself is used. I have mine doing delegation, handling default variables and stripping out (some) pattern matching in the delegate’s parameter definition, adding documentation, replacing module name or namespace references within the docs, marking the delegate as the implementation of a callback, and optionally allowing inlining of the implementation (rather than simply delegating), as well as some other library-specific stuff. I would like to get it doing specs as well post-refactor.


You would likely want to do something similar but less extensive in your use-case… but unless the functionality and extensibility of your code is predicated on such capabilities you’re probably saving tons of time and complexity by updating the duplicated documentation by hand.

1 Like