Why the dot (when calling anonymous functions)?
- José Valim
- August 14th, 2023
- elixir
In this article, I will explain why Elixir has a dot when calling anonymous functions. I have explained this elsewhere in forums and mailing lists but I guess an article makes it more official.
In other words, Elixir has this:
some_fun = fn x, y -> x + y end
some_fun.(1, 2)
#=> 3
Note the dot between the variable and the arguments. The main reason for this choice is because functions in Elixir have to be identified by name and arity (the number of arguments it receives).
A fictional language
In order to understand why the dot is required, let’s consider a fictional language that runs on the Erlang VM. Functions in the Erlang VM are identified by their name and their arity. In other words, we don’t have functions that receive a variadic number of arguments, they are always fixed.
Consequently, the following is not possible in Elixir:
plus = fn
() -> 0
(a, b) -> a + b
end
plus() #=> 0
plus(1, 2) #=> 3
But let’s imagine for a second this actually worked. Our functions have multiple arities and we don’t need a dot to call them. Now let’s proceed and define a function, called sum
, that adds all elements in a list. Our initial implementation could look like this:
def sum(list) do
plus = fn
() -> 0
(a, b) -> a + b
end
Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
end
Notice how I am calling plus
with a variadic number of arguments: first to get the initial reduce
argument and then to reduce each element. Calling sum([1, 2, 3])
would return 6.
Let’s keep moving forward with this fictional language. We figure out the plus
implementation is actually quite useful and decide to move it to its own function:
def plus(), do: 0
def plus(a, b), do: a + b
def sum(list) do
Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
end
The refactoring was a success as we just moved the definition out and everything still works. Note we didn’t have to change the actual sum
logic, as we can use plus()
to call a function stored in a variable or a function defined in the same module/context.
This is how languages like Clojure and Scheme behave. You could even go as far as doing something akin to:
def plus(), do: 0
def plus(a, b), do: a + b
def sum(list) do
# A one-off plus implementation
plus = fn
() -> 1
(a, b) -> a + b
end
Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
end
And now sum([1, 2, 3])
will return 7 due to the wrong initial value. In the example above, we introduced a variable plus
and it shadowed the call to the plus
function defined in the module. In other words, identifiers in those languages refer to both variables and functions and they can be used interchangeably. You know if a variable or a function will be used by analyzing the scope.
These languages are similar to Lisp-1 languages because there is a single namespace for both variables and function names. Other languages, such as Haskell, also have a single namespace, but they do not support overloading on the arity.
Back to Elixir
In order to understand the limitation within Elixir, let’s try to do the same change. Imagine we have this code, which is valid Elixir:
def plus(), do: 0
def plus(a, b), do: a + b
def sum(list) do
Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
end
And we want to introduce a one-off sum implementation without changing the actual sum
call, as we did in the previous section:
def sum(list) do
plus = ???
Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
end
Unfortunately, there is no possible implementation of ???
in Elixir that makes the code above work. That’s because anonymous functions in Elixir only have a single arity… so we can implement plus()
or plus(x, y)
but not both. In other words, because functions in the Erlang VM are identified by name and arity, such that definitions with the same name and different arities are effectively different functions, we can’t fully leverage the benefits of Lisp-1 languages.
Once again: why the dot?
With the context above, I had to answer the following question when designing Elixir: should anonymous functions have a dot when invoked or not?
We could skip the dot when calling anonymous function in Elixir but I believe doing so would be a net negative. If plus()
allowed invoking a module function and calling a function in a variable, we would introduce the ambiguity found in Lisp-1 languages but without its upsides.
Therefore, Elixir is a Lisp-2 language, where variables and function names live in two distinct namespaces. That’s ultimately the difference between Lisp-1 and Lisp-2, the number of “namespaces” they offer. Since they live in different namespaces, we need distinct function call syntaxes for each namespace. In turn, this comes with benefits for code readability and maintainability. Let’s take a look at our sample code again but with a different perspective:
def sum(list) do
plus = ???
Enum.reduce(list, plus(), fn x, y -> plus(x, y) end)
end
In Elixir, it is not possible to introduce a variable named plus
that will change the behaviour of the plus(...)
function calls right below it. This eliminates the chance of naming conflicts and can be a comforting guarantee when reading and writing code! On the other hand, Lisp-1 languages require you to analyze what is in scope in order to determine what is the exact code that plus(...)
will invoke. Which approach you prefer, expressiveness vs clarity, is the crux of the Lisp-1 vs Lisp-2 debate.
If we didn’t have the dot when calling anonymous functions in Elixir, we would have the worst of both worlds: we would lose clarity but be unable to fully leverage the expressiveness found in Lisp-1 languages.
But Erlang!
At this point, developers familiar with Erlang may point out that the dot is not required in Erlang. That’s because variables and function names in Erlang have different syntaxes, which puts them in distinct namespaces by definition. Variables start in uppercase, while function names in lowercase. Here is how an anonymous function in Erlang looks like:
Var = fun(X, Y) -> X + Y end,
Var(1, 2).
Similarly, Erlang has the same guarantees as Elixir in that it is not possible to introduce a variable that affects function calls happening within the same function - as the syntaxes differ:
plus() -> 0.
plus(A, B) -> A + B.
sum(List) ->
Plus = ???, % no such thing
lists:foldl(fun(X, Y) -> plus(x, y) end, plus(), Plus).
Similarly, to pass a module function as an anonymous function, explicit conversion is required (as in Lisp-2 languages):
Var = fun plus/2,
Var(1, 2).
Which is the same as in Elixir:
var = &plus/2
var.(1, 2)
In other words, the languages semantics are precisely the same. They simply use different syntactical constructs to disambiguate. The fact Elixir uses the dot and Erlang does not, does not add new capabilities to any of them. Other languages may not require the dot when calling anonymous functions either, but they may still use different syntaxes when calling those different namespaces.
Does this mean all languages running on the Erlang VM need to have those exact same semantics? Not necessarily. A statically typed language, for example, could support multiple arities in the same anonymous function and track how the different arities are used statically to still emit efficient code. Or even forbid multiple arities for the same name altogether!
Summing up
I hope this clarifies one of the most asked parts about the Elixir syntax and answers “Why the dot?”.
Truth be told, even if Elixir could have a single namespace for variables and funcfions, I would still keep the dot when calling anonymous functions, as the benefits if offers for those reading code are more important than flexibility in specific idioms.
TL;DR: given the lack of Tabs vs Spaces discussions nowadays, I resurface the Lisp-1 vs Lisp-2 debate to keep programming forums active.