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.
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:
# 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")) 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:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- 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.
- 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!".
- 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.
- 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
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:
# 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", ".")) 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
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:
# 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")) 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:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The definition. def hello(name \\ "world", punctuation \\ "!") has one body and two arguments, each with a default after \\. The body becomes hello/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.
- 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.
- 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
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:
# 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 ")) # 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")) # 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 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
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:
# 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() 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:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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
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:
defmodule Shop.Cart do
def total(prices), do: Enum.sum(prices)
end defmodule Shop.Checkout do
def pay(prices), do: Shop.Cart.total(prices, :inr)
end 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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
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:
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 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:
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:
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
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.
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.
-
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.
-
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?
Key takeaways
defdefines a public function anddefpa 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/0andhello/1are 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.Itemare 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-errorsturns 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
- Elixir guide: Modules and functions (The Elixir Team (hexdocs.pm))
- Kernel (Elixir): defmodule/2, def/2 and defp/2 (The Elixir Team (hexdocs.pm))
- Module (Elixir): __info__/1 (The Elixir Team (hexdocs.pm))
- UndefinedFunctionError (Elixir) (The Elixir Team (hexdocs.pm))
- Elixir guide: Naming conventions (The Elixir Team (hexdocs.pm))
- mix compile.elixir (Mix): --force and --warnings-as-errors (The Elixir Team (hexdocs.pm))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress