Elixir from the ground up

Functions in modules

Naming work: a module groups related functions, a function can have several heads that match different shapes of input, defaults make the common call short, and defp keeps helpers inside.

Part 6 of 12

Intermediate18 min readBack to Courses

A module is a drawer of named work

A module collects functions that belong together and gives them a home: defmodule Greeter do ... end, with def for each function inside. From the outside you call them with the module name in front, Greeter.hello("Ada"). That dot is all a module really is — a name you reach through. The module name is capitalised because it names a thing rather than a value, and the dot is the same dot you use to read a field from a map.

In a real project each module lives in its own file and the file is named after the module. Here everything is in one file, which is fine for learning and still reads the same way: the module name says what the drawer holds, and the function names say what each piece of work is called.

One function, many heads

A function can be written more than once with different patterns in its head, and Elixir tries the heads in the order you wrote them. The head is a match, so it both decides whether that version applies and names the parts it needs. This is where the pattern matching from earlier pays off: an if inside the body becomes a shape in the head. The head can match on a tuple, a list, a struct or a plain value, and the body can be a single expression or a do block.

Order is the whole design. A head that matches everything must come last, or the more particular heads below it are dead code. Read the heads from the top down as a series of questions, and check that each question can still be reached. A catch-all head at the top is the classic mistake: every later head becomes dead code, and the compiler may warn about a clause that can never match.

Defaults, and work that stays inside

A default argument is written with two backslashes before the value: def hello(name, greeting \ "Hello"). A caller who leaves it out gets the default, and a caller who passes it overrides it. That is how a function offers a short common call and a longer precise one without two names. The default is part of the definition, so a caller who passes nil is passing a value, not asking for the default; only leaving the argument out does that.

defp defines a private function — usable inside the module, invisible from outside. Private functions are how a module keeps its promises small: the public function states what the module does, and the private helpers are the steps it takes to get there. When you want to change how something is done, the private helper is the place, because nothing outside depends on it. A private function can also change its name or its arguments without breaking a caller, which is what makes it a safe place to put a step you are still shaping.

A default argument, a private helper, and a pipe inside

defmodule Greeter do
  # A default argument: the caller may leave it out.
  def hello(name, greeting \\ "Hello") do
    "#{greeting}, #{name}!"
  end

  def shout(text), do: String.upcase(text) <> "!"

  # defp keeps a helper inside the module.
  defp trim(text), do: String.trim(text)

  def clean_hello(name), do: hello(trim(name))
end

IO.puts(Greeter.hello("Ada"))
IO.puts(Greeter.hello("Ada", "Hi"))
IO.puts(Greeter.clean_hello("  Ada  "))

Two small modules

Run it, then add a function of your own to Greeter with a default argument, and change Bill so that it takes a list of prices and adds a fixed fee.

Output
Click "Run" to execute your code

A module is also the place to put an idea you would otherwise repeat. If two parts of a program both work out a total, the total belongs in one function that both call — and the name of that function is the shortest explanation of what the total means.

Arity is part of the name

hello/1 and hello/2 are different functions as far as Elixir is concerned, even though they share a name. A default argument is shorthand for writing both: def hello(name, greeting \\ "Hello") defines hello/1 and hello/2, and the one-argument call fills in the default. That is why a default belongs on a function head, above the clauses that use it. The header is a definition with no body: it declares the defaults once, and the clauses below it only match the arguments they receive.

Clauses of the same name and arity must sit next to each other. Elixir groups them by name and arity when it compiles, and a clause separated by another function is a warning — the reader cannot see the whole set in one place, and the compiler says so.

Calling a private function from outside the module fails, even if you know its name; defp is a promise to the module. A default argument and several clauses need a separate header, or the compiler complains that defaults are defined in multiple clauses. And an arity mismatch is not a type error — it is a missing function, reported as UndefinedFunctionError.

Try these next

  • Add a def hello(name, greeting \\ "Hello") and call it with one and with two arguments.
  • Move a private helper into a public function and observe what changes at the call site.
  • Write two heads for describe/1, one for {:ok, value} and one for {:error, reason}.

A temperature converter

Run it and check both directions against the numbers you know. Then add a `:kelvin` head, call it with `:kelvin`, and call it once with no unit to see which head answers.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

How do you call the function hello defined inside the module Greeter?