Closures can be used for lots of things, but at heart they’re about separating “what should be computed” from “when it should be computed”.
A good example to look at is Map.get/3
versus Map.get_lazy/3
:
Map.get(some_map, :some_key, ExpensiveModule.run())
# always executes ExpensiveModule.run, even when the value isn't used
Map.get_lazy(some_map, :some_key, fn -> ExpensiveModule.run() end)
# only executes ExpensiveModule.run if the value is needed
Here, wrapping ExpensiveModule.run()
(the “what should be computed”) in a closure allows Enum.get_lazy
to evaluate it on-demand (“when it should be computed”).
This operation is so common there’s a shorthand for it, the “capture operator” &
. The second call above could instead be spelled Map.get_lazy(some_map, :some_key, &ExpensiveModule.run/0)
(aside: a closure can also be handy if the value is sensitive and you don’t want it to appear in debug traces, etc)
Closures see two kinds of variables: arguments and environment.
- arguments are supplied where the closure is used. A simple example:
Enum.map(some_list, fn arg -> IO.inspect(arg) end)
- the closure’s environment is “closed” when the closure is created, and contains the local variables visible at that point in the code For instance,
def split_many(strings, sep) do
Enum.map(strings, fn s ->
String.split(s, sep)
end)
end
Here s
is an argument to the closure, but sep
is part of the environment.
The closure’s environment is part of the closure, so it doesn’t change even when the closure is passed around:
def splitter(sep) do
fn s ->
String.split(s, sep)
end
end
def split_many_with_splitter(strings, splitter) do
Enum.map(strings, splitter)
end
# called like
split_many_with_splitter(strings, splitter("\t"))
Here sep
is part of the environment in the closure returned by splitter
, still available despite the splitter
function having returned.
The BEAM takes this one level farther: code can send a closure to a different PROCESS and it will work without issues:
origin_pid = self()
closure = fn other_pid ->
IO.inspect(origin_pid, label: "origin pid")
IO.inspect(other_pid, label: "other pid")
end
listener_pid = spawn(fn ->
receive do
a_closure ->
a_closure.(self())
end
end)
send(listener_pid, closure)
should print something like:
origin pid: #PID<0.106.0>
#Function<44.97283095/1 in :erl_eval.expr/5>
other pid: #PID<0.152.0>
(the exact position of the lines is variable, because concurrency)
Here closure
is created in the initial process sent to the listener_pid
process, but retains the value in origin_pid
.
This is used in the standard library for things like Agent.get
, where it’s used to avoid copying the entire Agent state back to the calling process - the idea is that the closure passed to Agent.get
can access the agent’s state directly and return only what it’s interested in. For instance, you might use an Agent to maintain a large shared data structure in-memory and use Agent.get
with a closure that extracts a small piece.
This also works over Distributed Erlang, for the ultimate “bring the computation TO the data” experience. 
Edit: add some general Elixir notes
-
[:error, "two strings expected"]
: the usual convention for this kind of value is a tuple ({:error, "two strings expected"}
). The list form will work, but future readers may be confused
-
if is_bitstring(str || token) do
: this likely doesn’t do what you mean. It matches what we would say “if str
or token
is a bitstring do this” but it means “if str
is a bitstring, or str
is nil
and token
is a bitstring”. Consider if is_bitstring(str) and is_bitstring(token) do
or see below for another option
-
consider doing type-checking with pattern-matching to keep the main code path clear:
def split_sentence(str, token) when is_bitstring(str) and is_bitstring(token) do
...
end
def split_sentence(_, _), do: {:error, "two strings expected"}