Do you want to pick up from where you left of?
Take me there

Documentation

Documenting Elixir code.

Annotation

How much we comment and what makes quality documentation remains a contentious issue within the programming world. However, we can all agree that documentation is important for ourselves and those working with our codebase.

Elixir treats documentation as a first-class citizen, offering various functions to access and generate documentation for your projects. The Elixir core provides us with many different attributes to annotate a codebase. Let’s look at 3 ways:

Inline Documentation

Probably the simplest way to comment your code is with inline comments. Similar to Ruby or Python, Elixir’s inline comment is denoted with a #, frequently known as a pound, or a hash depending on where you are from in the world.

Take this Elixir Script (greeting.exs):

# Outputs 'Hello, chum.' to the console.
IO.puts("Hello, " <> "chum.")

Elixir, when running this script will ignore everything from # to the end of the line, treating it as throwaway data. It may add no value to the operation or performance of the script, however when it’s not so obvious what is happening a programmer should know from reading your comment. Be mindful not to abuse the single line comment! Littering a codebase could become an unwelcome nightmare for some. It is best used in moderation.

Documenting Modules

The @moduledoc annotator allows for inline documentation at a module level. It typically sits just under the defmodule declaration at the top of a file. The example below shows a one line comment within the @moduledoc decorator.

defmodule Greeter do
  @moduledoc """
  Provides a function `hello/1` to greet a human
  """

  def hello(name) do
    "Hello, " <> name
  end
end

We (or others) can access this module documentation using the h helper function within IEx. However, utilizing the h helper function immediately after creating a module in iex can lead to the following issue:

iex> h Greeter
Greeter was not compiled with docs

Reason: When code is entered into iex, Elixir’s interactive shell compiles it in memory without automatically writing the compiled code to disk. Saving compiled bytecode requires explicit instructions, either by explicitly saving the bytecode or by utilizing Elixir’s file I/O functions to write it to a file.

Consider a scenario where there’s a file named greeter.ex in the current working directory, and an iex session is launched from there:

iex> c("greeter.ex")
[Greeter]

iex> h Greeter
Greeter was not compiled with docs

Encountering the same issue here is expected due to the nature of c("greeter.ex"). This command compiles the file in memory but doesn’t automatically write the bytecode (.beam file) to disk. However, for the h/1 helper to function correctly, the .beam file needs to exist on disk.

To resolve this, it’s essential to instruct c to store the generated bytecode in the current directory. This action enables the h/1 helper to access and display the documentation as intended.

iex> c("greeter.ex", ".")
[Greeter]

iex> h Greeter

                Greeter

Provides a function hello/1 to greet a human

Note: we don’t need to manually compile our files as we did above if we’re working within the context of a mix project. You can use iex -S mix to load the IEx console for the current project if you’re working in a mix project.

Documenting Functions

Just as Elixir gives us the ability for module level annotation, it also enables similar annotations for documenting functions. The @doc annotator allows for inline documentation at a function level. The @doc annotator sits just above the function it is annotating.

defmodule Greeter do
  @moduledoc """
  ...
  """

  @doc """
  Prints a hello message.

  ## Parameters

    - name: String that represents the name of the person.

  ## Examples

      iex> Greeter.hello("Sean")
      "Hello, Sean"

      iex> Greeter.hello("pete")
      "Hello, pete"

  """
  @spec hello(String.t()) :: String.t()
  def hello(name) do
    "Hello, " <> name
  end
end

If we kick into IEx again and use the helper command (h) on the function prepended with the module name, we should see the following:

iex> c("greeter.ex", ".")
[Greeter]

iex> h Greeter.hello

                def hello(name)

  @spec hello(String.t()) :: String.t()

Prints a hello message.

Parameters

   name: String that represents the name of the person.

Examples

    iex> Greeter.hello("Sean")
    "Hello, Sean"

    iex> Greeter.hello("pete")
    "Hello, pete"

iex>

Notice how you can use markup within our documentation and the terminal will render it? Apart from really being cool and a novel addition to Elixir’s vast ecosystem, it gets much more interesting when we look at ExDoc to generate HTML documentation on the fly.

Note: the @spec annotation is used to statically analyze code. To learn more about it, check out the Specifications and types lesson.

ExDoc

ExDoc is an official Elixir project that can be found on GitHub. It produces HTML (HyperText Markup Language) and online documentation for Elixir projects. First let’s create a Mix project for our application:

$ mix new greet_everyone

* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/greet_everyone.ex
* creating test
* creating test/test_helper.exs
* creating test/greet_everyone_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd greet_everyone
    mix test

Run "mix help" for more commands.

$ cd greet_everyone

Now copy and paste the code from the @doc annotator lesson into a file called lib/greeter.ex and make sure everything is still working from the command line. Now that we are working within a Mix project we need to start IEx a little differently using the iex -S mix command sequence:

iex> h Greeter.hello

                def hello(name)

  @spec hello(String.t()) :: String.t()

Prints a hello message.

Parameters

   name: String that represents the name of the person.

Examples

    iex> Greeter.hello("Sean")
    "Hello, Sean"

    iex> Greeter.hello("pete")
    "Hello, pete"

Installing

Assuming all is well and we’re seeing the output above, we are now ready to set up ExDoc. In the mix.exs file, add the :ex_doc dependency to get started.

  def deps do
    [{:ex_doc, "~> 0.21", only: :dev, runtime: false}]
  end

We specify the only: :dev key-value pair as we don’t want to download and compile the ex_doc dependency in a production environment.

ex_doc will also add another library for us, Earmark.

Earmark is a Markdown parser for the Elixir programming language that ExDoc utilizes to turn our documentation within @moduledoc and @doc to beautiful looking HTML.

It is worth noting at this point that you can change the markup tool to Cmark if you wish, but you will need to do a little more configuration which you can read about here. For this tutorial, we’ll just stick with Earmark.

Generating Documentation

Carrying on, from the command line run the following two commands:

$ mix deps.get # gets ExDoc + Earmark.
$ mix docs # makes the documentation.

Docs successfully generated.
View them at "doc/index.html".

If everything went to plan, you should see a similar message as to the output message in the above example. Let’s now look inside our Mix project and we should see that there is another directory called doc/. Inside is our generated documentation. If we visit the index page in our browser we should see the following:

ExDoc Screenshot 1

We can see that Earmark has rendered our Markdown and ExDoc is now displaying it in a useful format.

ExDoc Screenshot 2

We can now deploy this to GitHub, our own website, or more commonly HexDocs.

Best Practice

Documentation should be added within the Best Practices Guidelines of the language. Since Elixir is a fairly young language many standards are still to be discovered as the ecosystem grows. The community, however, tried to establish best practices. To read more about best practices see The Elixir Style Guide.

defmodule Greeter do
  @moduledoc """
  This is good documentation.
  """

end
defmodule Greeter do
  @moduledoc false

end
defmodule Greeter do
  @moduledoc """
  ...

  This module also has a `hello/1` function.
  """

  def hello(name) do
    IO.puts("Hello, " <> name)
  end
end
defmodule Greeter do
  @moduledoc """
  ...

  This module also has a `hello/1` function.
  """

  alias Goodbye.bye_bye
  # and so on...

  def hello(name) do
    IO.puts("Hello, " <> name)
  end
end
defmodule Greeter do
  @moduledoc """
  ...
  """

  @doc """
  Prints a hello message

  ## Parameters

    - name: String that represents the name of the person.

  ## Examples

      iex> Greeter.hello("Sean")
      "Hello, Sean"

      iex> Greeter.hello("pete")
      "Hello, pete"

  """
  @spec hello(String.t()) :: String.t()
  def hello(name) do
    "Hello, " <> name
  end
end
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!