Today I'm going to talk about documentation in Elixir. We're already familiar with code comments starting with the #
character. That's about all many languages offer, and any documentation is done within code comments or somewhere else entirely. Elixir, however, goes beyond that with significantly more support for module and function documentation. Elixir has special syntax for documenting modules and functions, which provides information for anyone using those functions and modules.
Module Documentation
Modules are documented with the @moduledoc
attribute, which is typically located just under the module name. Module documentation can sometimes be fairly lengthy, and it typically explains everything you need to know to use the module. It's not uncommon for the majority of a module source file to be occupied by module documentation with a small section of code below it.
This is what module documentation looks like:
defmodule SomeModule do
@moduledoc """
This is where the module documentation goes.
It describes in detail what this module is for and how to use it.
"""
end
The @moduledoc
attribute should be located just under the defmodule
line.
Function Documentation
Functions are documented using the @doc
attribute, which is placed above the function being documented.
@doc """
Does something with some parameters.
What does this actually do? We don't actually know, which makes this documentation
pretty useless. To see something even more useless, look at the documentation
for `do_something_else/2`.
## Parameters
- param1: the first parameter
- param2: the second parameter
## Examples
iex> SomeModule.do_something("Yum", :food)
"Some output"
iex> SomeModule.do_something(3, 4)
12
"""
def do_something(param1, param2) do
end
Documentation Contents
Unlike documentation schemes like Javadoc, doxygen, or the standard style of C# documentation, Elixir does not give you a certain documentation structure that you must follow with certain keywords that identify parameters and return types. There are certain conventions that are used regularly, but otherwise you have a lot of flexibility in how you structure your documentation. The only real requirement is that you use markdown syntax, since that's how it is interpreted when the HTML version of the documentation is generated. This type of documentation is meant to be read by humans and not parsed by tools. That's not to say that there isn't some Elixir doc tool out there that wants a more fixed format, but the doc tools that come with Elixir don't require that.
Let's go over the documentation conventions.
- The first line of any documentation should be a simple summary of what something does. Documentation tools will typically use this to generate a summary. So the typical practice is to make the first line a summary that is its own paragraph. The next paragraph can then go into detail.
- Markdown syntax in documentation is encouraged, and the Elixir documentation itself makes considerable use of markdown syntax.
- Use markdown second level headers (
##
) to denote sections. First-level markdown section headers (#
) are reserved for use by the documentation generator. I imagine that any lower-level section headers (###
and so forth) can be used without issues. - Any functions or modules mentioned in documentation should be enclosed in back ticks (
`SomeModule.function_name/1`
), which is also markdown notation for something that should be formatted as code. Not only will anything enclosed in back ticks be formatted as code, but the documentation will also automatically generate links for modules and functions enclosed in back ticks. - The entire namespace of a module should be specified. So a module should be
Group.Category.Module
instead of justModule
. This will allow the documentation generator to generate links to the appropriate module. - Function names should contain the name and arity of a function, since that will allow the documentation generator to generate links to that function
- Documentation should go before the first clause of multi-clause functions only. Do not specify documentation for each clause, since the documentation is for the function, not a particular clause.
- Show examples of the function being called in an "## Examples" section. The examples are just printouts of an IEx session and are always indented with two tabs (which are converted to four space characters by Elixir conventions).
Documentation Examples
Here's an example of what documentation looks like from real Elixir code. This is the documentation for Enum.all?/2
from the Elixir source code.
@doc """
Returns `true` if the given `fun` evaluates to a truthy value (neither `false` nor `nil`)
on all of the items in the `enumerable`.
It stops the iteration at the first invocation that returns either `false` or `nil`.
## Examples
iex> Enum.all?([2, 4, 6], fn x -> rem(x, 2) == 0 end)
true
iex> Enum.all?([2, 3, 4], fn x -> rem(x, 2) == 0 end)
false
If no function is given, it defaults to checking if
all items in the `enumerable` are truthy values.
iex> Enum.all?([1, 2, 3])
true
iex> Enum.all?([1, nil, 3])
false
"""
Compare that to the Enum.all?/2
documentation to see how it looks in HTML form.
Documentation Tools
There appear to be two tools that are used for generating Elixir documentation, ExDoc and Earmark. It took a bit of reading to figure out which tool does what, but here's how I understand it. ExDoc, which is built and maintained by the Elixir maintainers, but is available as a separate package, converts the documentation in the source code into HTML. Then Earmark, which is also available as a package, runs through and converts the Markdown inside the documentation into HTML-formatted documentation.
That's how I think it works. It won't matter much though, because that all happens invisibly to us. We just have to invoke the magic and we'll get some documentation.
Generating Documentation - A Walkthrough
I'm going to go through generating documentation for an example project. I created a new project called "doc_project" with `mix new doc_project". You can either follow along or view the complete project in the the "lwm 64 - Documentation" folder in the code examples in the Learn With Me: Elixir repository on Github.
First of all, I'm going to edit mix.exs to add ExDoc and Earmark as dependencies.
defp deps do
[
{:ex_doc, "~> 0.19.3", only: [:dev]},
{:earmark, "~> 1.3", only: [:dev]}
]
end
I added only: [:dev]
to the dependency to indicate that this dependency only needs to be downloaded and built for a Dev environment.
Then I need to specify sufficient project metadata in mix.exs. This will help ExDoc produce better documentation. I modified the project/0
function in mix.exs by adding the name:
and source_url:
information to what is already there. ExDoc uses the source URL to generate links to the source code.
def project do
[
app: :doc_project,
version: "0.1.0",
name: "Doc Project",
elixir: "~> 1.8",
source_url: "https://github.com/Maultasche/LwmElixirCode/tree/master/examples/lwm%2064%20-%20Documentation/doc_project",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
Once that's done, I'll retrieve the dependencies using mix deps.get
.
> mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
earmark 1.3.1
ex_doc 0.19.3
makeup 0.8.0
makeup_elixir 0.13.0
nimble_parsec 0.5.0
* Getting ex_doc (Hex package)
* Getting earmark (Hex package)
* Getting makeup_elixir (Hex package)
* Getting makeup (Hex package)
* Getting nimble_parsec (Hex package)
Now I need to write some code with some documentation. I replaced the generated code in "lib/doc_project.ex" with this:
defmodule DocProject do
@moduledoc """
This is the main (and only) module for the doc project, which is intended
to demonstrate documentation.
The documentation in this module will be used for generating HTML documentation.
It will be formatted real nice and pretty with pleasing colors and links.
Yeah, links! Pretty fancy stuff. All it needs now are some unicorns and rainbows,
but we ran out.
"""
@doc ~S"""
Converts an enumerable into string form
The contents of the enumerable will be iterated over, converted to a string
using `Kernel.inspect/1` and put in a comma-separated list surrounded by brackets.
## Examples
iex> DocProject.stringify([])
"[]"
iex> DocProject.stringify(["a", "b", "c"])
"[\"a\", \"b\", \"c\"]"
iex> DocProject.stringify(1..10)
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
"""
def stringify(enumerable) do
comma_string = enumerable
|> Enum.map(fn element -> inspect(element) end)
|> Enum.join(", ")
"[" <> comma_string <> "]"
end
@doc """
Sums the numbers in an enumerable.
If the enumerable does not contain all numbers, then there will be trouble.
## Examples
iex> DocProject.sum([1, 2, 3, 4, 5])
15
iex> DocProject.sum(1..10)
55
iex> DocProject.sum([22])
22
iex> DocProject.sum([])
0
"""
def sum(enumerable) do
Enum.reduce(enumerable, 0, &Kernel.+/2)
end
end
There are two functions, DocProject.stringify/1
and DocProject.sum/1
. I put in some documentation documenting the module and the functions, with IEx examples. This is what will form the generated documentation.
Once that was done, and I did a little ad-hoc testing in IEx to verify that the code actually worked, I added the project to Github so that the source code links could be generated correctly.
Then to generate the documentation, I'll run mix docs
. The mix tool will build the dependencies and then generate the documentation.
> mix docs
==> nimble_parsec
Compiling 4 files (.ex)
Generated nimble_parsec app
==> makeup
Compiling 45 files (.ex)
Generated makeup app
==> earmark
Compiling 1 file (.yrl)
Compiling 2 files (.xrl)
Compiling 3 files (.erl)
Compiling 25 files (.ex)
Generated earmark app
==> makeup_elixir
Compiling 4 files (.ex)
Generated makeup_elixir app
==> ex_doc
Compiling 18 files (.ex)
Generated ex_doc app
==> doc_project
Compiling 1 file (.ex)
Generated doc_project app
Docs successfully generated.
View them at "doc/index.html".
The documentation can be found in the "doc" subdirectory. You can load "index.html" in browser to look at the documentation. You can view also view the documentation for the DocProject module by clicking on the link in this sentence. Compare the generated HTML to the documentation in the source code file.
Note that the source code link (the </> symbols to the right of the module and function headers) won't go to the correct place. This is because ExDoc assumes that the project source code is located in the root its own git repository. That's not the case for this project, which is in a subdirectory of a repository. It seems that there's nothing that can be done in this case to make that link work correctly.
Documentation in IEx
We can also view the documentation in IEx. I'm going to start the project in IEx and use the "h" command to view the documentation for the module and an individual function.
> iex -S mix
Compiling 1 file (.ex)
Generated doc_project app
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex> h DocProject
DocProject
This is the main (and only) module for the doc project, which is intended to
demonstrate documentation.
The documentation in this module will be used for generating HTML
documentation. It will be formatted real nice and pretty with pleasing colors
and links. Yeah, links! Pretty fancy stuff. All it needs now are some unicorns
and rainbows, but we ran out.
iex> h DocProject.stringify
def stringify(enumerable)
Converts an enumerable into string form
The contents of the enumerable will be iterated over, converted to a string
using Kernel.inspect/1 and put in a comma-separated list surrounded by
brackets.
## Examples
iex> DocProject.stringify([])
"[]"
iex> DocProject.stringify(["a", "b", "c"])
"[\"a\", \"b\", \"c\"]"
iex> DocProject.stringify(1..10)
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
All the documentation is available from IEx as well, which is great if we just want to quickly see some information about how to use something. The output looks better in the terminal window because there are foreground and background colors being used.
Here's a screenshot of what the documentation for DocProject.stringify/1
looks like in iex.
Documentation as Tests
Here's what is really neat about Elixir. Not only do the IEx examples in the documentation serve as a great tool for anyone reading the documentation, but they also double as unit tests. This is called a doctest in Elixir. Documention that is also a unit test? Whaaaat? Why? At least that was my first reaction. I didn't understand why that was useful at first, but I soon came to understand.
The tool that is running unit tests can also scan a module and run any doctests that it finds. It looks in the documentation in the source code files for any indentation of 4 spaces (which is 2 tab indentations under the Elixir coding conventions, since each tab is converted into 2 space characters) followed by an iex>
prompt. The test tool will take that command and run it in IEx, comparing the result that IEx gives it to the result shown in your documentation.
Not only does this doctest feature allow you to write documentation and unit tests at the same time, but it also helps verify that the examples in your documentation stay accurate and current. It won't ensure that all your documentation is always correct, but it can help you keep it correct by catching errors in the examples. A doctest failure is either a sign that your code is broken or is a clear signal to update your documentation for that function. Doctests are not intended to replace unit tests, and are not expected to be extensive, but serve more as a supplement to unit tests. They are documentation first and tests second, so when in doubt, choose your examples with a human reader in mind.
We'll go more into how to run doctests and the more traditional code-based unit tests in a later post, but for now just be aware of the effect your documentation examples can have.
I was previously ambivalent about doctests when I first heard that they existed, but now that I've learned more about them and can see their benefit, I think they're fantastic.
Improving Your Documentation
I recommend looking at existing documentation, comparing the documentation in the source file to the generated documentation. This will help you get a better idea of what is possible. The Elixir source code is on Github and is a good place to start. For example, you could take look at the Enum module source code and compare it to the Enum module documentation.
The more effort you put into your documentation, the better the generated documentation will be, and the easier it will be for other people to use your code.