Elixir & Erlang/OTP Module 4
Functions and modules interview questions
Interview questions on functions and modules: short warm-ups, then what each harder question tests, the first step of a good answer and its follow-ups.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
What is a closure in Elixir, and what does it capture?
Answered in the lesson Anonymous functions, closures and capture
-
Fresher
What is the difference between def and defp, and why are Elixir functions written as name/arity?
Answered in the lesson Modules, named functions and default arguments
-
Fresher
Why write several function clauses with guards instead of one function with a case or an if inside?
-
Fresher
What does the pipe operator do in Elixir, and when do you use then/2?
-
Fresher
What is the difference between alias, require, import and use in Elixir?
-
Fresher
What are module attributes used for in Elixir?
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A report starts one Task.async/1 for each of 2,000 orders with fn -> score(order, prices) end, where prices is a list of 10,000 integers loaded once. Memory climbs by hundreds of megabytes on every run. Explain what each task receives, estimate the cost from the lesson's measurements, and redesign the code.
- What it tests
- Knowing that a closure carries its captured values into every process that runs it, estimating the cost of the copies, and choosing what to capture and where shared data should live.
- First step
- Find what the closure captures: it uses order and prices, so every task gets its own copy of the whole list. The lesson measured a process running a closure over that list at 230,280 bytes, against 2,624 for one over a single number, so 2,000 tasks alive at once hold about 460 MB of copies.
- Follow-ups
-
- Each order needs only the prices of its own items. What do you capture then?
- Every task really needs the whole list. Where do you keep it?
- How do you stop all 2,000 tasks from running at once?
From the lesson Anonymous functions, closures and capture
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A teammate adds up an order list with total = 0 and Enum.each(orders, fn o -> total = total + o.amount end), and the report always prints 0. They suggest keeping the total somewhere global instead. Explain what happens, what the compiler already said, and how you would write it.
- What it tests
- Knowing that a name bound inside a function stays inside it, reading the compiler's unused-variable warning, and replacing changing state with Enum.reduce/3 or Enum.sum/1.
- First step
- Read the warning the compiler printed for that line: the total inside the function is unused, and there is a variable with the same name in the context. Each call binds its own new total, which is gone when the call ends; the outer total is never changed, so it stays 0.
- Follow-ups
-
- The warning suggests the pin operator. Would ^total help here?
- How do you count the paid orders and add up their amounts in one pass?
- When is Enum.each/2 the right choice?
From the lesson Anonymous functions, closures and capture
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
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.
- What it tests
- Knowing that a default adds a lower arity, that two definitions of one name and arity collide, and choosing between a default and an explicit function without breaking callers.
- First step
- List the arities: the definition with a default adds build/0, and the module already has an explicit build/0 above it. Elixir 1.20.4 warns that this clause for build/0 cannot match because a previous clause always matches: the explicit one runs, and the default :html is never used.
- Follow-ups
-
- What does the compiler say when the definition with the default comes first?
- Callers already call build(:pdf). What must the change keep working?
- Would a keyword list of options be better than a default?
From the lesson Modules, named functions and default arguments
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
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?
- What it tests
- Knowing that nested names give no access, that defp is the only rule about who may call a function, and keeping internal modules internal with a narrow public module and a check in the build.
- First step
- Check what nesting gives: Payments.Internal.Keys is a separate module whose name starts with Payments, and its public functions can be called from anywhere, as the lesson's Shop.Cart.Item was. Only defp restricts calls, and only to the one module that defines the function.
- Follow-ups
-
- How do you list every file that depends on Payments.Internal.Keys?
- Does @moduledoc false stop the calls?
- Three Payments modules need the helpers. Where do they go?
From the lesson Modules, named functions and default arguments
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
Shipping had def fee(amount) when is_integer(amount) and amount >= 0, do: 4_000. A teammate added a clause below it, when is_integer(amount) and amount >= 50_000, returning 0. Orders of 60,000 paise still pay 4,000, and the build printed no warning. Explain both, fix it, and name the tests that prove it.
- What it tests
- Ordering clauses from specific to general, knowing which overlaps the Elixir 1.20 checker reports and which it cannot, and testing every band at its edges.
- First step
- Trace fee(60_000): the first clause is tried first, 60,000 is an integer and at least 0, so it returns 4,000 and the new clause never runs. Then ask why no warning came: the first guard compares, so the checker, which reasons about types, cannot tell that the clause hides the one below.
- Follow-ups
-
- Which amounts do you test, and why those?
- When would Elixir 1.20 have warned about the same mistake?
- A third band, 2,000 from 20,000, is coming. Where does it go?
From the lesson Guards and multi-clause functions
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate writes def register(%{email: email} = user) when valid_email?(email), and Elixir refuses it. They then try a defguard built on String.contains?/2 and are refused again. Explain why both fail, and design register/1 so that each kind of bad input gets a clear result.
- What it tests
- Knowing what guards may hold and why, what defguard can and cannot do, and moving validation into ordinary code whose results the clauses then match.
- First step
- Read the two errors: a local function cannot be invoked inside a guard, and a defguard body is checked like a guard, so String.contains?/2 is refused as a remote function. Guards hold only built-ins without side effects, so the check has to run as ordinary code before any clause chooses.
- Follow-ups
-
- Which parts of the check can stay in a guard?
- Where should the validation live as the rules grow?
- How do you test that each bad input gets its own result?
From the lesson Guards and multi-clause functions
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
To find a failing test, a teammate added dbg/2 to a pipeline. The CI log now shows lines like ^[[36m^[[3m[lib/report.ex:12: Report.build/1]^[[0m, and setting NO_COLOR=1 in CI changed nothing. Explain the codes, how to get readable output, and how to keep such calls out of the main branch.
- What it tests
- Knowing what dbg/2 prints and that it writes colour codes even into a pipe or a log, turning them off with syntax_colors, and keeping debugging calls out of committed code.
- First step
- Read the line as the lesson's cat -v did: ^[ is the escape character and [36m a colour code. dbg/2 writes those codes whatever receives its output, a terminal, a pipe or a log file, and NO_COLOR=1 did not stop them; its own option, syntax_colors: [], does.
- Follow-ups
-
- Why use dbg/2 in a failing test at all?
- How do you make sure no dbg/2 call reaches the main branch?
- Would IO.inspect/2 have had the same problem?
From the lesson Pipes, dbg/2 and debugging in IEx
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A function misbehaves only on one customer's data. You run the service with iex -S mix and want to stop the function on that customer's request and look at its values. Explain how you do it, what IEx asks first, and what can go wrong while the request is paused.
- What it tests
- Using break! on code that runs in another process, knowing that IEx asks before it pries, narrowing a breakpoint with a pattern, and the effects of a paused process on its callers.
- First step
- Set the breakpoint from the prompt, so no code changes: break! with the function and, to stop only for that customer, a pattern on its argument. The request runs in its own process, not the shell's, so IEx asks first whether to allow the pry; then read the bindings, step with next, finish with continue.
- Follow-ups
-
- The request times out while you look around. Why, and what do you do?
- How do you stop on that one customer's call only?
- When would you use IEx.pry/0 instead of break!?
From the lesson Pipes, dbg/2 and debugging in IEx
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A module defines and calls its own then/2. After an Elixir upgrade it stops compiling with "imported Kernel.then/2 conflicts with local function". Explain why an unchanged module broke, give a fix that works on both versions, and say how a team keeps upgrades from breaking like this.
- What it tests
- Knowing that every module imports Kernel automatically, that new Kernel functions are an expected change between releases, and the backwards-compatible fix with import Kernel, except:.
- First step
- Every module imports Kernel automatically, and the newer Elixir added then/2 to Kernel. A local then/2 that the module calls now has the same name and arity as an import, so the compiler cannot tell which one the call means and refuses it, as the lesson's Stock module was refused over max/2.
- Follow-ups
-
- Why not simply rename the local function?
- Which other directive problems surface on upgrades?
- How do you find such breakage before the upgrade reaches main?
From the lesson alias, require, import and use
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
After use SomeLib was added to a module, the module's own format/1 stopped compiling with "imported SomeLib.Helpers.format/1 conflicts with local function", although nothing in the module imports SomeLib.Helpers. Explain how, how you would see what use brought in, and what you would change in the module and in the library.
- What it tests
- Knowing that use runs the library's __using__/1, which can write imports into your module, inspecting a use line, and the anti-patterns guide's advice on use instead of import.
- First step
- Expand the line: use SomeLib becomes require SomeLib and a call to SomeLib.__using__/1, and the code that call returns goes into the module. If it holds import SomeLib.Helpers, every function of Helpers is now imported, format/1 included, and it clashes with the module's own format/1.
- Follow-ups
-
- How do you see the code a use line writes?
- Keep the use, or replace it?
- What should the library change?
From the lesson alias, require, import and use
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A payments module has @base_url System.get_env("PAYMENT_URL", "https://sandbox.payments.example"). The release is built in CI, where PAYMENT_URL is not set, and in production every charge goes to the sandbox although the servers set PAYMENT_URL to the live address. Explain, fix it, and stop it from happening again.
- What it tests
- Knowing that module attributes are evaluated at compile time, separating build-time from run-time configuration, and checks that catch configuration frozen into a build.
- First step
- Find when the line runs: an attribute is evaluated while the module compiles, so System.get_env/2 ran in CI, found no PAYMENT_URL and wrote the sandbox default into the compiled module. The servers' variable is never read, as the lesson's compiled_url/0 ignored the live address.
- Follow-ups
-
- Where should PAYMENT_URL be read instead?
- When is reading configuration at compile time right?
- How do you catch this before production?
From the lesson Module attributes, docs and doctests
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A team writes all its examples as doctests. The one for Report.save/1 writes a file into the project folder, another expects #PID<0.150.0>, and CI fails now and then while files pile up. Explain what goes wrong, and decide which examples stay doctests.
- What it tests
- Knowing what doctests are for and what they are not, from the ExUnit documentation, and splitting examples between doctests and ordinary tests.
- First step
- Read the ExUnit documentation's advice: doctests are not recommended when examples contain side effects. They do not run in a sandbox, so a written file stays behind, and a result such as a pid is compared as text and changes from run to run, so it cannot match reliably.
- Follow-ups
-
- What should the @doc of Report.save/1 show instead?
- How do you test the writing of the file?
- What makes a good doctest?
From the lesson Module attributes, docs and doctests
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
All Elixir & Erlang/OTP interview questions · The Elixir & Erlang/OTP track