Tracks
/
Elixir
Elixir
/
Exercises
/
City Office
City Office

City Office

Learning Exercise

Introduction

Docs

Documentation in Elixir is a first-class citizen.

There are two module attributes commonly used to document your code - @moduledoc for documenting a module and @doc for documenting a function that follows the attribute. The @moduledoc attribute usually appears on the first line of the module, and the @doc attribute usually appears right before a function definition, or the function's typespec if it has one. The documentation is commonly written in a multiline string using the heredoc syntax.

Elixir documentation is written in Markdown.

defmodule String do
  @moduledoc """
  Strings in Elixir are UTF-8 encoded binaries.
  """

  @doc """
  Converts all characters in the given string to uppercase according to `mode`.

  ## Examples

      iex> String.upcase("abcd")
      "ABCD"

      iex> String.upcase("olĂĄ")
      "OLÁ"
  """
  def upcase(string, mode \\ :default)
end

Typespecs

Elixir is a dynamically typed language, which means it doesn't provide compile-time type checks. Still, type specifications can be used as a form of documentation.

A type specification can be added to a function using the @spec module attribute right before the function definition. @spec is followed by the function name and a list of all of its arguments' types, in parentheses, separated by commas. The type of the return value is separated from the function's arguments with a double colon ::.

@spec longer_than?(String.t(), non_neg_integer()) :: boolean()
def longer_than?(string, length), do: String.length(string) > length

Types

Most commonly used types include:

  • booleans: boolean()
  • strings: String.t()
  • numbers: integer(), non_neg_integer(), pos_integer(), float()
  • lists: list()
  • a value of any type: any()

Some types can also be parameterized, for example list(integer) is a list of integers.

Literal values can also be used as types.

A union of types can be written using the pipe |. For example, integer() | :error means either an integer or the atom literal :error.

A full list of all types can be found in the "Typespecs" section in the official documentation.

Naming arguments

Arguments in the typespec could also be named which is useful for distinguishing multiple arguments of the same type. The argument name, followed by a double colon, goes before the argument's type.

@spec to_hex({hue :: integer, saturation :: integer, lightness :: integer}) :: String.t()

Custom types

Typespecs aren't limited to just the built-in types. Custom types can be defined using the @type module attribute. A custom type definition starts with the type's name, followed by a double colon and then the type itself.

@type color :: {hue :: integer, saturation :: integer, lightness :: integer}

@spec to_hex(color()) :: String.t()

A custom type can be used from the same module where it's defined, or from another module.

Instructions

You have been working in the city office for a while, and you have developed a set of tools that speed up your day-to-day work, for example with filling out forms.

Now, a new colleague is joining you, and you realized your tools might not be self-explanatory. There are a lot of weird conventions in your office, like always filling out forms with uppercase letters and avoiding leaving fields empty.

You decide to write some documentation so that it's easier for your new colleague to hop right in and start using your tools.

1. Document the purpose of the form tools

Add documentation to the Form module that describes its purpose. It should read:

A collection of loosely related functions helpful for filling out various forms at the city office.

2. Document filling out fields with blank values

Add documentation and a typespec to the Form.blanks/1 function. The documentation should read:

Generates a string of a given length.

This string can be used to fill out a form field that is supposed to have no value.
Such fields cannot be left empty because a malicious third party could fill them out with false data.

The typespec should explain that the function accepts a single argument, a non-negative integer, and returns a string.

3. Document splitting values into lists of uppercase letters

Add documentation and a typespec to the Form.letters/1 function. The documentation should read:

Splits the string into a list of uppercase letters.

This is needed for form fields that don't offer a single input for the whole string,
but instead require splitting the string into a predefined number of single-letter inputs.

The typespec should explain that the function accepts a single argument, a string, and returns a list of strings.

4. Document checking if a value fits a field with a max length

Add documentation and a typespec to the Form.check_length/2 function. The documentation should read:

Checks if the value has no more than the maximum allowed number of letters.

This is needed to check that the values of fields do not exceed the maximum allowed length.
It also tells you by how much the value exceeds the maximum.

The typespec should explain that the function accepts two arguments, a string and a non-negative integer, and returns one of two possible values. It returns either the :ok atom or a 2-tuple with the first element being the :error atom, and the second a positive integer.

5. Document different address formats

For some unknown to you reason, the city office's internal system uses two different ways of representing addresses - either as a map or as a tuple.

Document this fact by defining three custom public types:

  • address_map - a map with the keys :street, :postal_code, and :city. Each key holds a value of type string.
  • address_tuple - a tuple with three values - street, postal_code, and city. Each value is of type string. Differentiate the values by giving them names in the typespec.
  • address - can be either an address_map or an address_tuple.

6. Document formatting the address

Add documentation and a typespec to the Form.format_address/1 function. The documentation should read:

Formats the address as an uppercase multiline string.

The typespec should explain that the function accepts one argument, an address, and returns a string.

Edit via GitHub The link opens in a new window or tab
Elixir Exercism

Ready to start City Office?

Sign up to Exercism to learn and master Elixir with 57 concepts, 158 exercises, and real human mentoring, all for free.