Have you ever worked on the project which contains no documentation? Did you have the desire to add documentation but didn’t know where to start? Well, now we can check how we can do that in Elixir and see how powerful that functionality is.

To show examples of writing documentation in Elixir I will use the following project as an example. It is simply a new project everyone can generate using mix new command with minimal changes. More info how to generate a new project you can read in my article Elixir - first steps.

# lib/todo_list_inbox.ex
defmodule TodoList.Inbox do
  def header do
    "Inbox"
  end
end
# test/todo_list_inbox_test.exs
defmodule TodoList.InboxTest do
  use ExUnit.Case

  test "returns the header" do
    assert TodoList.Inbox.header() == "Inbox"
  end
end
→ mix test
Compiling 1 file (.ex)
.

Finished in 0.03 seconds
1 test, 0 failures

That is our green field project we will start to write our documentation from here.

ExDoc

To generate a nice looking HTML documentation we are going to use ExDoc tool.

That means we will need to add ExDoc tool as a dependency into our project. To do that we need to extend the list of our dependencies with this item {:ex_doc, "~> 0.18", only: :dev, runtime: false} You can read more about that tool on its GitHub page. For now, that means we are going to use ex_doc library of version 0.18 (at the time of publishing this article) or higher and only in development environment.

So our deps function in the mix.exs file should look like:

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

To install dependencies into our project we need to use the mix deps.get command in the terminal window from the project directory.

→ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
  earmark 1.2.3
  ex_doc 0.18.1
All dependencies up to date

Now as our dependency is set up and ready we can start writing a documentation for the project.

Documenting modules

Let’s start by adding a description to our module. It will describe what our module is intended to do. We would use @moduledoc attribute and we put it on the top of the file inside the module. The value of the attribute or the documentation body should be enclosed in triple quotes """. You can read about module attributes here.

Now our module should look in the following way:

defmodule TodoList.Inbox do
  @moduledoc """
    Provides methods for creating and handling a ToDo List
  """

  def header do
    "Inbox"
  end
end

Cool. Now we are ready to generate our documentation. The mix docs command will do all the dirty work for us.

→ mix docs
==> earmark
Compiling 3 files (.erl)
Compiling 24 files (.ex)
Generated earmark app
==> ex_doc
Compiling 15 files (.ex)
Generated ex_doc app
==> todo_list
Compiling 1 file (.ex)
Generated todo_list app
Docs successfully generated.
View them at "doc/index.html".

Now we can open doc/index.html in the browser to see what do we get. On the main page, we can see the list of our existing modules. In our case, it is a single TodoList.Inbox module which contains description we have created.

On the left side of the page, we can see the name of our project, version, search bar and a menu. The name and the version come from mix.exs file and can be updated for your needs.

defmodule TodoList.Mixfile do
  use Mix.Project

  def project do
    [
      app: :todo_list,
      version: "0.1.0"
      # ...
    ]
  end

  # ...

end

Clicking on the module name in this list will bring us to the documentation page of this particular module where we can see more information. For example list of the functions this module provides.

Here we can see that our module has a single function header/0 and that function has no documentation. Let’s jump in and add documentation for that function.

Documenting functions

We can achieve it in the similar way of how we did it for the module, but we should use @doc module attribute instead. This time we will add this right above our function definition.

@doc """
  Returns a header of the inbox folder
"""
def header do
  "Inbox"
end

Now we should regenerate our docs using mix docs command and open the docs/index.html file again.

Now if we navigate to the module page we can see our function has the description.

OK, that is a simple function. Let’s try to add one more function to the documentation as another example.

@doc """
  Adds a new item to the `list` with the description which is provided by `description` attribute.

## Examples

    iex> todo_list = TodoList.Inbox.add_item([], "First item")
    iex> [first_item | _] = todo_list
    iex> first_item[:description]
    "First item"

"""
def add_item(list, description) do
  list ++ [%{description: description}]
end

In this example, we have introduced a new function add_item/2. The function accepts two arguments: a list and item’s description. Once we call that function it will add a new item to the list we are providing.

First, you can see we are using backtick (`) symbol to highlight our arguments. ExDoc supports Markdown inside the documentation and we can use backtick the same way how do we use it in the Markdown.

Next, we have an example section which requires their own format:

First, we start it on the new line with two # symbols followed by the space and the Examples word.
Next, we have an indentation for four spaces followed by iex> text, space and then an actual working example. We can have several iex> lines as you can see in your example.
Then we can provide the final result. The same result we can see in our Interactive Elixir.

Don’t worry you will understand the importance of this structure later.

Now let’s try to regenerate our documentation again and check our module page.

Now we can see a description of the add_item/2 function, we can see how do our list and description attributes have been highlighted in the description. And also we can see our well-defined example as a separate section.

Let’s try yet another cool thing that Elixir provides us.

Doctests

As you remember from the beginning of the article we even have a single unit test for our header/0 function. Let’s extend our test file by adding doctest TodoList.Inbox line.

defmodule TodoList.InboxTest do
  use ExUnit.Case
  doctest TodoList.Inbox

  test "returns the header" do
    assert TodoList.Inbox.header() == "Inbox"
  end
end

Let’s run our tests again

→ mix test
..

Finished in 0.06 seconds
2 tests, 0 failures

Now we can see we have two green tests. But where does the second test comes from? So in Elixir, there is two type of tests we can write. The one is using test keyword as we have in our file. The second type is so-called “doctest”. That is where magic comes from. Once we have and examples section in our documentation, the doctest will use that example to test our function. That is why our examples should be in the working state.

To see that in action let’s change the last line of our example to be “Second item” instead of “First item” and run our tests again.

→ mix test
Compiling 1 file (.ex)


  1) test doc at TodoList.Inbox.add_item/2 (1) (TodoList.InboxTest)
     test/todo_list_inbox_test.exs:3
     Doctest failed
     code: todo_list = TodoList.Inbox.add_item([], "First item")
            [first_item | _] = todo_list
            first_item[:description] === "Second item"
     left: "First item"
     stacktrace:
       lib/todo_list_inbox.ex:18: TodoList.Inbox (module)

.

Finished in 0.08 seconds
2 tests, 1 failure

Our test will fail and we can see that our documentation expects description to contain “Second item” string, but it is actually “First item”.

first_item[:description] === "Second item"
left: "First item"

Ain’t that cool?

Now we can avoid the situation where developers forgot to keep documentation up to date after changing the implementation of the function.

Summary

So now we know how to setup ExDoc to write our documentation. We know how to write documentation for modules and functions, how to describe examples of how to use that function. And also we have discovered how to use “doctests” to test our functions from examples we have provided.