elixir icon indicating copy to clipboard operation
elixir copied to clipboard

Provide an API to access documentation metadata at compile time

Open hauleth opened this issue 7 years ago • 11 comments

Environment

  • Elixir & Erlang/OTP versions (elixir --version): 1.7.2
  • Operating system: macOS

Current behavior

I wanted to create macro that would allow me to remove boilerplate over generating structures for events in my application. To document such structures I wanted to use @doc to simulate feeling that these are normal parts of the external module instead of being separate modules, ex.:

defmodule MyApp.Event do
  import Events

  @moduledoc false

  @doc """
  Creation of new submission
  """
  @doc deprecated: "Foo"
  defevent SubmissionCreated,
    id: any(),
    name: String.t(),
    author: [map()]
end

While basic implementation is dumb easy documentation is quite challenging. What I have achieved is:

defmodule Events do
  @moduledoc false

  defmacro defevent(module, fields \\ []) do
    keys = Keyword.keys(fields)

    quote do
      docs = Module.get_attribute(__MODULE__, :doc)
      deprecated = case Module.get_attribute(__MODULE__, :deprecated) do
        nil -> []
        value -> [deprecated: value]
      end

      {set, _} = :elixir_module.data_tables(__MODULE__)
      metadata = case :ets.lookup(set, {:doc, :meta}) do
        [{{:doc, :meta}, metadata, _}] -> deprecated ++ Keyword.new(metadata)
        [] -> deprecated
      end

      defmodule unquote(module) do
        if docs, do: Module.put_attribute(__MODULE__, :moduledoc, docs)
        if metadata != [], do: @moduledoc metadata

        @type t :: %__MODULE__{unquote_splicing(fields)}

        defstruct unquote(keys)
      end

      :elixir_module.delete_definition_attributes(__ENV__, nil, nil, nil, nil, nil)
    end
  end
end

Which is fairly ok, except of the part where I need manually get data from ETS for documentation metadata. It cannot be mitigated by using Module.get_attribute/2 as it explicitly requires atom as a second argument while metadata are stored under {:doc, :meta}.

Expected behavior

Somehow allow fetching documentation metadata in macros. This would allow macro writers to utilise that metadata in some way like example above.

hauleth avatar Aug 16 '18 20:08 hauleth

I would add "at compile time" to the title, because once compiled I think you can access the documentation using Code.fetch_docs/1 :)

fertapric avatar Aug 16 '18 20:08 fertapric

The documentation metadata and related fields is stored as private fields on the table, that's why you can't access it and you shouldn't rely on the compiler internals for this. :)

The other thing is that, even if we provide a way to access metadata, then you would also need a way to remove the documentation because there is no actual function definition. It feels like the best way to go here is to use your own attribute, such as @eventdoc, especially because you won't pretend that you are defining a function when it is actually a module.

josevalim avatar Aug 16 '18 20:08 josevalim

The proper solution here would be to make @doc and friends an accumulated attribute, so accessing @doc would return all previously set values:

@doc "foo"
@doc bar: 1
@doc #=> [{2, bar: 1}, {1, "foo"}]

This would also allow us to get rid of special cases in both Module and Kernel. The trouble with doing this now is that it will break code that expects @doc to always be a binary (or similar). So we can only do it on Elixir 2.0.

josevalim avatar Aug 16 '18 21:08 josevalim

@josevalim I agree that in this particular situation it makes sense to use custom attribute instead, but imagine something like that:

defmacro defrecord(name, tag \\ nil, kv) do
  quote do
    def unquote(name)(), do: …
    def unquote(name)(values), do: …
    def unquote(name)(record, values), do: …
  end
end

And while you maybe do not want to share most docs between them then currently supported tags makes sense for them (like :since or :deprecated).

hauleth avatar Aug 16 '18 21:08 hauleth

@josevalim Instead of this

@doc "foo"
@doc bar: 1
@doc #=> [{2, bar: 1}, {1, "foo"}]

could you consider this

@doc "foo"
@doc bar: 1
@doc #=> [doc: "foo", bar: 1]

?

mguimas avatar May 09 '19 15:05 mguimas

@mguimas unfortunately a metadata can be named :doc, so that would be ambiguous, we will probably use a tuple-based format, such as {"doc", meta: "data"}, or a map: %{doc: "...", metadata: [...]}.

josevalim avatar Jun 23 '20 08:06 josevalim

@josevalim I would vote for the tuple + keyword list so:

@doc "foo:
@doc bar: 1
@doc bar: 2

Would result with:

{"foo", [bar: 1, bar: 2]}

hauleth avatar Jun 23 '20 13:06 hauleth

@josevalim I believe that a good data structure is one that makes it is easy to use Enum to traverse the information associated with the @doc entries.

So in the example

@doc :foo
@doc bar: 1
@doc bar: 2
@doc :bar

perhaps a good solution is

[:foo, {:bar, 1}, {:bar, 2}, :bar]

which respects not only the contents, but also the order of the doc entries.

mguimas avatar Jun 23 '20 17:06 mguimas

@hauleth @josevalim It's been 3 years; I don't think this issue has been resolved, but I figured I'd check in with both of you to verify. 😄

Nezteb avatar Dec 22 '23 16:12 Nezteb

It’s not. It’s marked for Elixir v2.0.

wojtekmach avatar Dec 22 '23 18:12 wojtekmach

It’s marked for Elixir v2.0.

Ah my bad. I'm on mobile and didn't scroll down to see the addition of the 2.0 milestone. 🤦‍♂️

Disregard me!

Nezteb avatar Dec 22 '23 18:12 Nezteb