Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Elixir & Erlang/OTP  Module 4 – Functions and modules

Modules, named functions and default arguments

Group Elixir functions into modules with def and defp, call them by name and arity, write default arguments and function heads, and nest module names.

  • Beginner
  • 20 minutes
  • Examples run with elixir 1.20.4, erlang 29.1.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define a module with public and private functions and call them by name and arity
  • Predict which arities a definition with default arguments generates
  • Write a bodiless function head for the defaults of a function with several clauses
  • Organise related modules under nested names without expecting the names to grant access
  • Catch calls to functions that do not exist when a Mix project compiles

Before you start

What you’ll learn · Before you start

What you will learn

  • Define a module with public and private functions and call them by name and arity
  • Predict which arities a definition with default arguments generates
  • Write a bodiless function head for the defaults of a function with several clauses
  • Organise related modules under nested names without expecting the names to grant access
  • Catch calls to functions that do not exist when a Mix project compiles

Before you start

On this page

Greeter.hello("Asha") calls the function hello/1 of the module: A named group of functions, defined with defmodule Greeter do … end. Its name is an atom, such as :"Elixir.Greeter", and other code calls its public functions with the name in front: Greeter.hello("Asha"). Greeter, and the /1 is part of the function’s name: give the same module a definition with two default arguments and it holds three functions, hello/0, hello/1 and hello/2. Call an arity a module lacks and the error names exactly that arity, “function Greeter.hello/2 is undefined or private”, and knowing why is most of this lesson.

Below, you will define modules with public and private functions, call them by name and arity, write default arguments and the function head that several clauses need, nest module names and see what nesting does not do, and let a Mix project find a wrong call before anything runs. Every session here used the pinned pair, Elixir 1.20.4 with Erlang/OTP 29.1.1.

A module groups functions, and defp keeps one private

defmodule Greeter do … end makes a module, and each def inside it defines a named function. Another module or a script calls it with the module’s name in front: Greeter.hello("Asha"). A function defined with defp is a private function: A function defined with defp, which only the module that defines it can call. A call from any other module or script fails with UndefinedFunctionError, saying the function is undefined or private.: the Kernel documentation says private functions “are only accessible from within the module in which they are defined”. This script defines two public functions and one private helper, then calls the helper from outside:

greeter.exs: two public functions and a private one Elixir · greeter.exs
# A module of named functions: two public ones, and a private helper only the module itself may call.
defmodule Greeter do
  def hello(name) do
    "Hello, " <> name <> "!"
  end

  def loud(name), do: shout(hello(name))

  defp shout(text), do: String.upcase(text)
end

IO.puts(Greeter.hello("Asha"))
IO.puts(Greeter.loud("Ravi"))
IO.inspect(Greeter.__info__(:functions))
IO.puts(Greeter.shout("from outside"))
Running greeter.exs Python · greeter_session.py
from session import run

# The last line calls the private function from outside the module.
run("""
!elixir greeter.exs
""")

Output

$ elixir greeter.exs
Hello, Asha!
HELLO, RAVI!
[hello: 1, loud: 1]
** (UndefinedFunctionError) function Greeter.shout/1 is undefined or private
    Greeter.shout("from outside")
    greeter.exs:15: (file)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 greeter_session.py

hello/1 and loud/1 ran. loud/1 called shout/1 without a module name, a local call from inside the module, which a private function accepts. The script’s own call to Greeter.shout/1 failed with UndefinedFunctionError, “is undefined or private”: from outside, a private function does not exist. Greeter.__info__(:functions), which every module has, listed only hello: 1 and loud: 1, the functions the module exports. Step through the calls:

Interactive · step by step Greeter: which calls reach which functions Step through the four calls of greeter.exs. The left column is the script outside the module, the right column the module.
Greeter: which calls reach which functions, step 4 of 4. Showing The script, def hello(name), def loud(name), defp shout(text), UndefinedFunctionError. In focus: UndefinedFunctionError.Greeter.helloGreeter.louda local callGreeter.shoutThe scriptoutside Greeterdef hello(name)public: Greeter.hello/1def loud(name)public: Greeter.loud/1defp shout(text)private: shout/1UndefinedFunctionError"undefined or private"not exported: missing from __info__

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The module. Greeter defines hello/1 and loud/1 with def, which other modules and the script may call, and shout/1 with defp, which only Greeter itself may call.
  2. A public call. Greeter.hello("Asha") names the module, the function and, by its one argument, the arity. hello/1 is public, so the call runs and returns "Hello, Asha!".
  3. A call from inside. Greeter.loud("Ravi") is public too. Inside the module it calls hello/1 and then shout/1 without a module name: a local call, which a private function accepts.
  4. A private call from outside. Greeter.shout("from outside") names a private function from outside, so it fails: UndefinedFunctionError, "is undefined or private". __info__(:functions) had listed only hello and loud.

loud/1 is written with do: on one line. The guide’s advice: use do: for one-liners and do blocks for functions that span several lines, as hello/1 does.

Checkpoint

Check: Public and private functions of a module

  1. Question 1 of 2 A script calls Greeter.shout("x"), and Greeter defines shout/1 with defp. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises UndefinedFunctionError, saying the function is undefined or private

    Private functions are only accessible from within the module that defines them, so a call from the script fails with "is undefined or private".

  2. Question 2 of 2 A module defines def hello(name), def loud(name) and defp shout(text). What does its __info__(:functions) return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [hello: 1, loud: 1]

    Greeter's list held hello: 1 and loud: 1. The arity is part of each entry because it is part of each function's name.

A function is its name and its arity

Elixir names a function by its name and its arity together. hello/0 and hello/1 share a name, yet they are two functions, each with its own body, as different as hello/1 and loud/1:

names.exs: hello/0 and hello/1 Elixir · names.exs
# hello/0 and hello/1 share a name, and they are two different functions.
defmodule Greeter do
  def hello, do: "Hello, world!"
  def hello(name), do: "Hello, " <> name <> "!"
end

IO.inspect(Greeter.__info__(:functions))
IO.puts(Greeter.hello())
IO.puts(Greeter.hello("Asha"))

say = &Greeter.hello/1
IO.puts(say.("Ravi"))

IO.puts(Greeter.hello("Asha", "."))
Running names.exs Python · names_session.py
from session import run

# The last line asks for hello/2, which the module does not define.
run("""
!elixir names.exs
""")

Output

$ elixir names.exs
[hello: 0, hello: 1]
Hello, world!
Hello, Asha!
Hello, Ravi!
** (UndefinedFunctionError) function Greeter.hello/2 is undefined or private. Did you mean:

    * hello/0
    * hello/1

    Greeter.hello("Asha", ".")
    names.exs:14: (file)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 names_session.py

__info__(:functions) listed both, and each call picked its function by how many arguments it gave. The capture &Greeter.hello/1 named one of them exactly, which is why a capture always carries the arity. The call with two arguments asked for hello/2, which does not exist, and the error said so in those terms: “function Greeter.hello/2 is undefined or private”, followed by the arities that do exist, hello/0 and hello/1. When you read such an error, compare the arity in it with the arities the module offers before you look for a typo in the name.

Reading hello/2 as a call to hello with a wrong argument

The message does not say that hello got a bad argument: it says that no function hello/2 exists at all. Pass the number of arguments one of the listed functions takes, or define the missing arity.

Checkpoint

Check: A function is its name and its arity

  1. Question 1 of 2 Which body runs for Greeter.hello()?

    Read the code, then choose one answer.

    defmodule Greeter do
      def hello, do: "Hello, world!"
      def hello(name), do: "Hello, " <> name <> "!"
    end
    Show the answer: Question 1 of 2

    Answer: The first, hello/0, which returns "Hello, world!"

    hello/0 and hello/1 are two functions. The number of arguments in the call decides which one runs.

  2. Question 2 of 2 A log says: function Shop.Cart.total/2 is undefined or private. Did you mean: * total/1. What went wrong?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Some code called total with two arguments, and Shop.Cart defines total with one

    Read the arity first: the call gave two arguments, and only total/1 exists. Change the call, or define the arity it needs.

Default arguments add the lower arities

A default argument: An argument written with \\ and a value, as in def hello(name \\ "world"), that a caller may leave out. Each default adds a function of one argument fewer, and the default is worked out on every call that uses it. is written after \\: def hello(name \\ "world") lets a caller leave name out. The Kernel documentation says the compiler “translates this into multiple functions with different arities”. With two defaults, one definition gives three:

defaults.exs: two defaults, and a default worked out per call Elixir · defaults.exs
# Two default arguments: one definition, three arities.
defmodule Greeter do
  def hello(name \\ "world", punctuation \\ "!") do
    "Hello, " <> name <> punctuation
  end
end

IO.inspect(Greeter.__info__(:functions))
IO.puts(Greeter.hello())
IO.puts(Greeter.hello("Asha"))
IO.puts(Greeter.hello("Asha", "."))

# A default is worked out on each call that needs it, never when the module is compiled.
defmodule Receipt do
  def default_note do
    IO.puts("(working out the default note)")
    "thank you"
  end

  def line(item, note \\ default_note()), do: item <> ": " <> note
end

IO.puts(Receipt.line("tea"))
IO.puts(Receipt.line("coffee"))
IO.puts(Receipt.line("water", "no ice"))
Running defaults.exs Python · defaults_session.py
from session import run

run("""
elixir defaults.exs
""")

Output

$ elixir defaults.exs
[hello: 0, hello: 1, hello: 2]
Hello, world!
Hello, Asha!
Hello, Asha.
(working out the default note)
tea: thank you
(working out the default note)
coffee: thank you
water: no ice

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 defaults_session.py

__info__(:functions) listed hello: 0, hello: 1 and hello: 2, and each call filled in what it left out: hello() both defaults, hello("Asha") the punctuation only. Step through what the definition becomes:

Interactive · step by step One definition with two defaults, three functions Step through what Greeter.hello/2 with two defaults becomes. The left column holds the calls, the right column the functions.
One definition with two defaults, three functions, step 4 of 4. Showing One definition, hello/2, hello/1, hello/0, Greeter.hello("Asha"), Greeter.hello(). In focus: Greeter.hello(), hello/0.definesfills punctuationfills bothOne definitiontwo default argumentshello/2the body you wrotehello/1calls hello(name, "!")hello/0calls hello("world", "!")Greeter.hello("Asha")gives "Hello, Asha!"Greeter.hello()gives "Hello, world!""world", "!"

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. The definition. def hello(name \\ "world", punctuation \\ "!") has one body and two arguments, each with a default after \\. The body becomes hello/2.
  2. The lower arities. For each default the compiler adds a function of one argument fewer that calls hello/2 with the default filled in. That is why __info__(:functions) listed hello: 0, hello: 1 and hello: 2.
  3. One argument. Greeter.hello("Asha") has one argument, so it is a call to hello/1, which passes "Asha" and the default "!" on to hello/2.
  4. No arguments. Greeter.hello() calls hello/0, which fills in both defaults. A default is worked out at each such call, which is why Receipt printed its note once per call that used it.

The second module shows when a default is worked out. “(working out the default note)” was printed once for “tea” and once for “coffee”, and not for “water”, which brought its own note. The guide says a default expression is not evaluated when the function is defined, but every time the function is invoked and the default has to be used. A default that calls a function, reads the clock or makes a new id therefore gives a fresh value on every call. Defaults belong to named functions only: the Kernel documentation notes that \\ cannot be used with anonymous functions, because each of those has a single arity.

Key idea

Every default adds a function of one argument fewer, so hello(name \\ "world", punctuation \\ "!") is hello/0, hello/1 and hello/2, and a default is worked out on each call that leaves its argument out.

Checkpoint

Check: Default arguments and the arities they add

  1. Question 1 of 2 How many functions does def build(a, b \\ 1, c \\ 2) give the module?

    Type a number.

    A hint

    Each default adds a function of one argument fewer.

    Show the answer: Question 1 of 2

    Answer: 3 functions

    build/3 is the body, and the two defaults add build/2 and build/1, so __info__(:functions) would list build: 1, build: 2 and build: 3.

  2. Question 2 of 2 Receipt.line(item, note \\ default_note()) is called as line("tea"), line("coffee") and line("water", "no ice"). How many times does default_note/0 run?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2 times

    A default is worked out on every call that leaves its argument out. Two calls left the note out, so the line "(working out the default note)" appeared twice; the call that brought its own note never ran default_note/0.

Several clauses take their defaults from a function head

Label.join/3 joins two parts with a separator, and returns the first part alone when the second is nil. That takes two clauses, and the separator should default to a space. With several clauses, the defaults go in a function head: A definition without a body, such as def join(first, second, separator \\ " "), written once above the clauses of a function to declare its defaults. It may have neither patterns nor guards.: a definition with no body, written once above the clauses. The guide says it is required in this case, and that a function head may have neither patterns nor guards. The session runs the same function written three ways:

heads/label.exs: the default in a function head Elixir · heads/label.exs
# Several clauses with a default: the default goes in a function head, a definition without a body.
defmodule Label do
  def join(first, second, separator \\ " ")
  def join(first, nil, _separator), do: first
  def join(first, second, separator), do: first <> separator <> second
end

IO.inspect(Label.__info__(:functions))
IO.puts(Label.join("New", "Delhi"))
IO.puts(Label.join("Pune", nil))
IO.puts(Label.join("Mumbai", "Pune", " to "))
heads/first_clause.exs: the default in the first clause Elixir · heads/first_clause.exs
# The default written in the first clause only: it compiles, with a warning.
defmodule Label do
  def join(first, nil, _separator \\ " "), do: first
  def join(first, second, separator), do: first <> separator <> second
end

IO.puts(Label.join("New", "Delhi"))
heads/every_clause.exs: the default in every clause Elixir · heads/every_clause.exs
# The default written in every clause: refused.
defmodule Label do
  def join(first, nil, _separator \\ " "), do: first
  def join(first, second, separator \\ " "), do: first <> separator <> second
end
The three versions Python · heads_session.py
from session import run

# The same function three ways: with a head, with the default in the first clause, and in both clauses.
run("""
cd heads
elixir label.exs
elixir first_clause.exs
!elixir every_clause.exs
""")

Output

$ cd heads
$ elixir label.exs
[join: 2, join: 3]
New Delhi
Pune
Mumbai to Pune
$ elixir first_clause.exs
    warning: def join/3 has multiple clauses and also declares default values. In such cases, the default values should be defined in a header. Instead of:

        def foo(:first_clause, b \\ :default) do ... end
        def foo(:second_clause, b) do ... end

    one should write:

        def foo(a, b \\ :default)
        def foo(:first_clause, b) do ... end
        def foo(:second_clause, b) do ... end

    the previous clause is defined on line 3

    │
  4 │   def join(first, second, separator), do: first <> separator <> second
    │       ~
    │
    └─ first_clause.exs:4:7

New Delhi
$ elixir every_clause.exs
    error: def join/3 defines defaults multiple times. Elixir allows defaults to be declared once per definition. Instead of:

        def foo(:first_clause, b \\ :default) do ... end
        def foo(:second_clause, b \\ :default) do ... end

    one should write:

        def foo(a, b \\ :default)
        def foo(:first_clause, b) do ... end
        def foo(:second_clause, b) do ... end

    the previous clause is defined on line 3

    │
  4 │   def join(first, second, separator \\ " "), do: first <> separator <> second
    │       ^
    │
    └─ every_clause.exs:4:7

** (CompileError) every_clause.exs: cannot compile file (errors have been logged)
    every_clause.exs:4: (module)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 heads_session.py

Where the default is written What Elixir 1.20.4 did
In a function head above the clauses Compiled; join: 2 and join: 3, every call worked
In the first clause only Compiled with a warning that the defaults “should be defined in a header”
In every clause Refused: join/3 “defines defaults multiple times”

The warning and the error print the shape to use instead: def foo(a, b \\ :default) first, then the clauses without defaults. The head names its arguments plainly, so it doubles as the function’s signature for a reader. Note that a clause which ignores an argument still has to take it, as _separator does in the nil clause.

Checkpoint

Check: Function heads for defaults

  1. Question 1 of 2 Why does a function with several clauses declare its defaults in a function head?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Defaults belong to the whole function, so they are written once, above all of its clauses

    Write def join(first, second, separator \\ " ") once without a body, then the clauses without defaults, as the compiler's own message suggests.

  2. Question 2 of 2 Which version compiles without an error or a warning?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: def size(list, unit \\ :items), then def size([], _unit) and def size(list, unit)

    One head above the clauses holds every default; the clauses then match their patterns as usual.

Nested module names are only names

Shop.Cart.Item looks like a part of Shop.Cart, and in the file it can even be written inside it. To the VM it is just another name. The guide says a module name is an alias that becomes an atom when the code compiles, so String is the atom :"Elixir.String", and that nested modules are effectively independent:

cart.exs: Shop.Cart, Shop.Cart.Item and Shop.Cart.Peek Elixir · cart.exs
# Nested names: Shop.Cart and Shop.Cart.Item are two separate modules. Nesting gives no access to private functions.
defmodule Shop.Cart do
  defmodule Item do
    def new(name, paise), do: {name, paise}
  end

  def add(cart, name, paise), do: [Item.new(name, paise) | cart]
  def total(cart), do: Enum.sum(for {_name, paise} <- cart, do: paise)

  defp discount_rate, do: 10
end

defmodule Shop.Cart.Peek do
  def rate, do: Shop.Cart.discount_rate()
end

IO.inspect(Shop.Cart.Item)
IO.inspect(Shop.Cart.Item == :"Elixir.Shop.Cart.Item")
IO.inspect(Code.ensure_loaded?(Shop))

cart = Shop.Cart.add([], "tea", 2_500)
cart = Shop.Cart.add(cart, "biscuits", 3_000)
IO.inspect(Shop.Cart.total(cart))

Shop.Cart.Peek.rate()
Running cart.exs Python · cart_session.py
from session import run

# The last line asks a nested module for its parent's private function.
run("""
!elixir cart.exs
""")

Output

$ elixir cart.exs
    warning: function discount_rate/0 is unused
    │
 10 │   defp discount_rate, do: 10
    │        ~
    │
    └─ cart.exs:10:8: Shop.Cart (module)

Shop.Cart.Item
true
false
5500
** (UndefinedFunctionError) function Shop.Cart.discount_rate/0 is undefined or private
    Shop.Cart.discount_rate()
    cart.exs:25: (file)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cart_session.py

Shop.Cart.Item printed as itself and equalled :"Elixir.Shop.Cart.Item". Code.ensure_loaded?(Shop) was false: the dotted names made no module called Shop. Inside Shop.Cart, the short name Item worked, because, in the documentation’s words, “when nesting, Elixir automatically creates an alias to the inner module”. And Shop.Cart.Peek, a module under the same name, could not call the private discount_rate/0: the compiler had even warned that nothing in Shop.Cart used it. Step through the names and the calls:

Interactive · step by step Shop.Cart.Item: a name, not a place inside Shop.Cart Step through cart.exs. Each box is a module name, which is an atom; the arrows are the calls the script makes.
Shop.Cart.Item: a name, not a place inside Shop.Cart, step 4 of 4. Showing Shop, Shop.Cart, Shop.Cart.Item, Shop.Cart.Peek, defp discount_rate, UndefinedFunctionError. In focus: Shop.Cart.Peek, UndefinedFunctionError.Item.new, an aliasShop.Cart.discount_rateShopno such moduleShop.Cartthe atom :"Elixir.Shop.Cart"Shop.Cart.Itema module of its ownShop.Cart.Peekdefined outside Shop.Cartdefp discount_rateprivate to Shop.CartUndefinedFunctionError"undefined or private"

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Two modules. defmodule Item inside defmodule Shop.Cart defines two modules, Shop.Cart and Shop.Cart.Item. The script printed Shop.Cart.Item == :"Elixir.Shop.Cart.Item" as true: a module name is an atom.
  2. No parent module. Code.ensure_loaded?(Shop) returned false: nesting made the name Shop.Cart, not a module called Shop. The guide calls the modules effectively independent.
  3. A short name inside. Inside Shop.Cart the short name Item works, because nesting sets an alias for the nested module in that scope. Outside, it needs its full name.
  4. No access from the name. Shop.Cart.Peek sits under the same name, yet its call to the private discount_rate/0 failed with UndefinedFunctionError: only Shop.Cart itself may call it.

Nesting is for organising names. What decides who may call a function is def or defp, nothing else.

Nesting a module to keep it internal

Payments.Internal.Keys can be called from any module of any application, exactly like Payments. Keep helpers that one module needs as defp in that module; nesting only groups the names.

Checkpoint

Check: Nested module names

  1. Question 1 of 2 Shop.Cart defines defp discount_rate. Can Shop.Cart.Peek, defined in the same file, call Shop.Cart.discount_rate()?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No; the call fails with UndefinedFunctionError, as cart.exs showed

    What decides who may call a function is def or defp. Nesting groups names and gives a short alias inside the parent.

  2. Question 2 of 2 A program defines only defmodule Shop.Cart. Does a module named Shop exist?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; Code.ensure_loaded?(Shop) returned false

    The guide says you do not have to define Foo before Foo.Bar, because they are effectively independent.

A Mix project checks every call when it compiles

In a script, a call to a function that does not exist fails only when its line runs: names.exs printed four lines before the call to hello/2 stopped it. A Mix project finds the same mistake while it compiles. The project below has one module a file under lib, each file named after its module in snake_case, as the naming conventions guide describes, and Shop.Checkout calls Shop.Cart.total/2, which does not exist:

shop/lib/shop/cart.ex Elixir · shop/lib/shop/cart.ex
defmodule Shop.Cart do
  def total(prices), do: Enum.sum(prices)
end
shop/lib/shop/checkout.ex Elixir · shop/lib/shop/checkout.ex
defmodule Shop.Checkout do
  def pay(prices), do: Shop.Cart.total(prices, :inr)
end
mix compile, then mix compile --warnings-as-errors Python · shop_session.py
from session import run

# One module a file, the path following the name. Mix finds the call to a function that does not exist while it
# compiles, then refuses to finish when warnings count as errors.
run("""
cd shop
mix compile
!mix compile --force --warnings-as-errors
""")

Output

$ cd shop
$ mix compile
Compiling 2 files (.ex)
    warning: Shop.Cart.total/2 is undefined or private. Did you mean:

        * total/1

    │
  2 │   def pay(prices), do: Shop.Cart.total(prices, :inr)
    │                                  ~
    │
    └─ lib/shop/checkout.ex:2:34: Shop.Checkout.pay/1

Generated shop app
$ mix compile --force --warnings-as-errors
Compiling 2 files (.ex)
    warning: Shop.Cart.total/2 is undefined or private. Did you mean:

        * total/1

    │
  2 │   def pay(prices), do: Shop.Cart.total(prices, :inr)
    │                                  ~
    │
    └─ lib/shop/checkout.ex:2:34: Shop.Checkout.pay/1

Compilation failed due to warnings while using the --warnings-as-errors option
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shop_session.py

The first compile built both modules and then warned that Shop.Cart.total/2 is undefined or private, with the file, the line and the arity that does exist, total/1, before any code ran. It still generated the app: a warning does not stop a build. The second compile, forced to rebuild with --force, ran with --warnings-as-errors, which the Mix documentation describes as “exit with non-zero status if compilation has one or more warnings”, and it failed with exit status 1. Follow the two compiles:

Interactive · stage by stage mix compile on the shop project Step through the two compiles of the session. Each stage is something Mix does before it says the app is built.
mix compile on the shop project, step 5 of 5. Showing Read lib/**/*.ex, Compile each module, Check calls between them, Print the warning, Generated shop app. In focus: Generated shop app.Read lib/**/*.ex2 files, 2 modulesCompile each moduleShop.Cart, Shop.CheckoutCheck calls between themShop.Cart.total/2?Print the warningdid you mean total/1Generated shop appor fail on a warning--warnings-as-errorsexit status 1 on the second run

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The files. mix compile reads every .ex file under lib, which is Mix's default source folder: lib/shop/cart.ex and lib/shop/checkout.ex, one module each, named after the module.
  2. The modules. It compiles Shop.Cart and Shop.Checkout. Either may come first, because a call to another module is only a name until the code runs.
  3. The calls. With every module of the project built, the compiler checks each call to another module against what that module defines. Shop.Checkout calls Shop.Cart.total/2, and Shop.Cart has only total/1.
  4. The warning. It warns that Shop.Cart.total/2 is undefined or private and suggests total/1, with the file and line, before any code has run.
  5. Built, or refused. A plain compile still generates the app. With --warnings-as-errors, Mix exits with a non-zero status instead, as the second compile did, so the mistake cannot reach a release.

Run your continuous integration with --warnings-as-errors, so that a call to a function that does not exist, an unused private function or a misplaced default never reaches a release.

Checkpoint

Check: Calls checked when a project compiles

  1. Question 1 of 2 When did the shop project report that Shop.Cart.total/2 does not exist?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: While mix compile ran, before any of the code ran

    With all modules of a project compiled, the calls between them can be checked, and a missing function is reported as a warning at compile time.

  2. Question 2 of 2 You want continuous integration to fail when code calls a function that does not exist. Which command does it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: mix compile --warnings-as-errors

    Warnings are cheap to ignore. --warnings-as-errors makes each one stop the build before a release can carry the mistake.

Practise: a temperature converter

Write Temperature.convert/3, which converts degrees from one unit to another: :c for Celsius, :f for Fahrenheit and :k for Kelvin. The target unit defaults to :c, so convert/2 exists too. It returns {:ok, value}, the value a float rounded to two decimals with Float.round/2, or {:error, :unit} for a unit it does not know. Copy the two files into an empty folder:

practice/temperature.ex: the starter Elixir · practice/temperature.ex
defmodule Temperature do
  @moduledoc "Converts temperatures between Celsius (:c), Fahrenheit (:f) and Kelvin (:k)."

  @doc """
  Converts `degrees` from the unit `from` to the unit `to`, which is :c when it is left out. Returns
  {:ok, value} with the value as a float rounded to two decimals, or {:error, :unit} for a unit it does not know.
  """
  def convert(_degrees, _from) do
    # Replace both functions with one definition that has a default argument.
    :todo
  end

  def convert(_degrees, _from, _to) do
    :todo
  end
end
practice/temperature_test.exs: the tests Elixir · practice/temperature_test.exs
Code.require_file("temperature.ex", __DIR__)
ExUnit.start(seed: 0)

defmodule TemperatureTest do
  use ExUnit.Case

  test "Celsius to Fahrenheit and back" do
    assert Temperature.convert(100, :c, :f) == {:ok, 212.0}
    assert Temperature.convert(-40, :c, :f) == {:ok, -40.0}
    assert Temperature.convert(212, :f, :c) == {:ok, 100.0}
  end

  test "the target unit is Celsius when it is left out" do
    assert Temperature.convert(98.6, :f) == {:ok, 37.0}
    assert Temperature.convert(300, :k) == {:ok, 26.85}
  end

  test "Kelvin in both directions" do
    assert Temperature.convert(0, :c, :k) == {:ok, 273.15}
    assert Temperature.convert(212, :f, :k) == {:ok, 373.15}
  end

  test "the same unit gives the same value, as a float" do
    assert Temperature.convert(20, :c, :c) == {:ok, 20.0}
  end

  test "an unknown unit is an error, not a crash" do
    assert Temperature.convert(10, :c, :x) == {:error, :unit}
    assert Temperature.convert(10, :rankine) == {:error, :unit}
  end

  test "convert/2 and convert/3 both exist" do
    assert Temperature.__info__(:functions) == [convert: 2, convert: 3]
  end
end

Run elixir temperature_test.exs. The starter defines convert/2 and convert/3 as two separate functions that return :todo, so only the test that asks for both arities passes:

The tests against the starter Python · practice_session.py
from session import run

# The tests against the starter, which converts nothing yet.
run("""
cd practice
!elixir temperature_test.exs
""")

Output

$ cd practice
$ elixir temperature_test.exs
Running ExUnit with seed: 0, max_cases: 36



  1) test Celsius to Fahrenheit and back (TemperatureTest)
     temperature_test.exs:7
     Assertion with == failed
     code:  assert Temperature.convert(100, :c, :f) == {:ok, 212.0}
     left:  :todo
     right: {:ok, 212.0}
     stacktrace:
       temperature_test.exs:8: (test)



  2) test the target unit is Celsius when it is left out (TemperatureTest)
     temperature_test.exs:13
     Assertion with == failed
     code:  assert Temperature.convert(98.6, :f) == {:ok, 37.0}
     left:  :todo
     right: {:ok, 37.0}
     stacktrace:
       temperature_test.exs:14: (test)



  3) test Kelvin in both directions (TemperatureTest)
     temperature_test.exs:18
     Assertion with == failed
     code:  assert Temperature.convert(0, :c, :k) == {:ok, 273.15}
     left:  :todo
     right: {:ok, 273.15}
     stacktrace:
       temperature_test.exs:19: (test)



  4) test the same unit gives the same value, as a float (TemperatureTest)
     temperature_test.exs:23
     Assertion with == failed
     code:  assert Temperature.convert(20, :c, :c) == {:ok, 20.0}
     left:  :todo
     right: {:ok, 20.0}
     stacktrace:
       temperature_test.exs:24: (test)



  5) test an unknown unit is an error, not a crash (TemperatureTest)
     temperature_test.exs:27
     Assertion with == failed
     code:  assert Temperature.convert(10, :c, :x) == {:error, :unit}
     left:  :todo
     right: {:error, :unit}
     stacktrace:
       temperature_test.exs:28: (test)

.
Finished in 0.01 seconds (0.01s on load, 0.00s async, 0.00s sync)

Result: 1/6 passed
Failed: 5 tests
[exit status 2]

This output changes from run to run: ExUnit prints how long the tests took, and max_cases follows the computer's processors

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

Replace both with one function head, def convert(degrees, from, to \\ :c), then a clause whose guard accepts the three known units on both sides, from in [:c, :f, :k] and to in [:c, :f, :k], and a last clause that returns {:error, :unit} for anything else. Inside, two private functions keep the formulas apart: one turns any unit into Celsius, the other Celsius into any unit, so six directions need six short clauses instead of six formulas. A finished version passes all six tests:

The tests against a finished convert/3 Python · finished_session.py
from session import run

# The same tests against a finished convert/3.
run("""
cd finished
cp ../practice/temperature_test.exs .
elixir temperature_test.exs
""")

Output

$ cd finished
$ cp ../practice/temperature_test.exs .
$ elixir temperature_test.exs
Running ExUnit with seed: 0, max_cases: 36

......
Finished in 0.01 seconds (0.01s on load, 0.00s async, 0.00s sync)

Result: 6 passed

This output changes from run to run: ExUnit prints how long the tests took, and max_cases follows the computer's processors

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finished_session.py

Temperature Converter Check a conversion, such as 98.6 °F in Celsius, before you write a test that expects it.

Now test what stayed with you. The questions start where your answers are weakest, cover modules, arities, defaults, heads, names and the Mix check, and explain each choice the moment you make it:

Practice that adapts to you

Practise this lesson

17 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 Which functions of a module can another module call?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: Those defined with def

    Public functions are exported and listed by __info__(:functions); private ones are not.

  2. Question 2 of 17 What does M.total([2, 3]) return?

    Read the code, then choose one answer.

    defmodule M do
      def total(list), do: sum(list, 0)
    
      defp sum([], acc), do: acc
      defp sum([x | rest], acc), do: sum(rest, acc + x)
    end
    Show the answer: Question 2 of 17

    Answer: 5

    A public function can use private helpers freely; callers outside see only total/1.

  3. Question 3 of 17 A formatting helper is needed by Shop.Cart and by Shop.Invoice. How should it be defined?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: With def, in one module that both use, such as Shop.Format

    defp is for helpers of one module. Code that several modules share is a public function of a module they all call.

  4. Question 4 of 17 Are hello/0 and hello/1 one function with an optional argument?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: No; they are two functions that happen to share a name

    Elixir names a function by name and arity together, which is why captures and errors always say /0 or /1.

  5. Question 5 of 17 An error reads: function Report.build/3 is undefined or private. Did you mean: * build/1 * build/2. Which fix is most likely right?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: Find the call with three arguments and give it one or two, or add the third argument to build

    The arity in the message and the arities in "Did you mean" tell you exactly what to compare.

  6. Question 6 of 17 What does Bill.total(1_000) return?

    Read the code, then choose one answer.

    defmodule Bill do
      def total(amount, tax_percent \\ 18), do: amount + div(amount * tax_percent, 100)
    end
    Show the answer: Question 6 of 17

    Answer: 1180

    One default, two functions: total/1 and total/2.

  7. Question 7 of 17 Can an anonymous function made with fn have a default argument?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: No; the documentation says \\ cannot be used with anonymous functions, which have a single arity

    For an optional argument, use a named function with a default, or write two anonymous functions.

  8. Question 8 of 17 def new(title, id \\ next_id()) is called twice without an id. What do the two calls get?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: Two values from two separate calls to next_id()

    A default expression runs on each call that uses it. That is what makes a default such as next_id() safe.

  9. Question 9 of 17 What is a function head?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: A definition without a body, such as def join(first, second, separator \\ " "), that declares the defaults

    A head is written once, above the clauses, and has no body, no patterns and no guards.

  10. Question 10 of 17 May a function head match a pattern, such as def join(first, nil, separator \\ " ")?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: No; the guide says function heads cannot have patterns nor guards

    Put plain names in the head, and nil and the other patterns in the clauses below it.

  11. Question 11 of 17 What did Elixir 1.20.4 do with the version of join/3 that wrote the default in both clauses?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: It refused it: def join/3 "defines defaults multiple times"

    Defaults go in a head, once. The compiler's message shows the shape to use.

  12. Question 12 of 17 What is the module name Shop.Cart.Item to the VM?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: The atom :"Elixir.Shop.Cart.Item"

    The guide: the String alias translates by default to the atom :"Elixir.String".

  13. Question 13 of 17 Item is defined inside defmodule Shop.Cart. How does code in another module call its new/2?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: Shop.Cart.Item.new("tea", 2500), or Item.new after alias Shop.Cart.Item

    Nesting creates an alias inside the parent only. The alias directive, later in this module, does the same anywhere.

  14. Question 14 of 17 How do you make sure that only Shop.Cart can call a helper it uses?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: Define the helper with defp inside Shop.Cart

    Privacy is defp. Names, files and nesting organise code; they do not restrict calls.

  15. Question 15 of 17 In a script run with elixir, when is a call to a function that does not exist found?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: When the line with the call runs

    A Mix project checks the calls between its modules at compile time; a script finds a wrong call only when it reaches it.

  16. Question 16 of 17 The first mix compile of the shop project printed a warning. Did it build the app?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: Yes; it printed Generated shop app after the warning

    That is why the warning must be read: the app was built with a call that cannot work.

  17. Question 17 of 17 In the shop project, which file holds the module Shop.Checkout?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: lib/shop/checkout.ex

    The naming conventions guide: file names follow the snake_case of the module they define. The compiled code does not depend on them.

Interview questions

Warm-up: what is the difference between def and defp, and why are Elixir functions written as name/arity?

def defines a public function, which any module can call with the module’s name in front, such as Greeter.hello("Asha"); defp defines a private function, which only the module that defines it can call, and a call from outside fails with UndefinedFunctionError, “undefined or private”. A module exports only its public functions, which is what __info__(:functions) lists. Functions are written as name/arity because the arity is part of the name: hello/0 and hello/1 are two different functions with their own bodies, a capture names one of them exactly, as &Greeter.hello/1 does, and an error about a missing function names the arity it looked for. Default arguments add arities too: one definition with two defaults is hello/0, hello/1 and hello/2.

Two questions from changing a module that others already call:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Report has def build, do: build(:text). A teammate adds def build(format \\ :html) below it so callers can pick a format, and Report.build() keeps returning {:report, :text}, with only a compiler warning. Explain the warning, what happens if the two definitions swap places, and how you would design build.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Key handling lives in Payments.Internal.Keys, nested inside defmodule Payments, and the team assumes only Payments can call it. A new reporting module calls Payments.Internal.Keys.decrypt/1 directly. Was the assumption right? How do you find such callers and keep the module internal?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • def defines a public function and defp a private one, which only its own module can call; from outside, a private function is “undefined or private”.
  • A function is its name and its arity, so hello/0 and hello/1 are two functions, and an error names the exact arity that was missing.
  • Each default argument adds a function of one argument fewer, and a default is worked out on every call that leaves its argument out.
  • A function with several clauses declares its defaults once, in a bodiless function head above the clauses.
  • Nested module names such as Shop.Cart.Item are separate modules with dotted names, which give no access to each other’s private functions.
  • A Mix project checks every call to another module when it compiles, and --warnings-as-errors turns the warning into a failed build.

Terms in this lesson

default argument
An argument written with \\ and a value, as in def hello(name \\ "world"), that a caller may leave out. Each default adds a function of one argument fewer, and the default is worked out on every call that uses it.
function head
A definition without a body, such as def join(first, second, separator \\ " "), written once above the clauses of a function to declare its defaults. It may have neither patterns nor guards.
module
A named group of functions, defined with defmodule Greeter do … end. Its name is an atom, such as :"Elixir.Greeter", and other code calls its public functions with the name in front: Greeter.hello("Asha").
private function
A function defined with defp, which only the module that defines it can call. A call from any other module or script fails with UndefinedFunctionError, saying the function is undefined or private.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.