Elixir 0-Arity Functions: Drop the Parens?
A question from github: func
or func()
? And, my opinion...
tl;dr; consistent use of trailing parens increases code clarity by explicitly signaling whether a reference is being used as a function call or a variable. Style decisions are case by case, but I’d default to keeping the parens when calling a 0 arity function.
Elixir + Barewords
Elixir’s Ruby heritage is apparent in its expressive and flexible syntax -- Elixir is unabashed about adding syntactic sugar to make your life easier.
In particular, it carries over what Avdi Grimm calls Ruby’s
“barewords”, references without decoration which can refer to
different things depending on the context. While it sounds exotic,
barewords are fundamental to the Elixir, e.g. variable references
are barewords. thing
or Module.thing
is a bareword, while
thing()
or Module
are not. Functions without arguments may be
called using the function’s name as a bareword, i.e. func
and
func()
.
In Elixir, a bareword is evaluated by first checking the bareword’s
name (the bareword itself) against variables in the context. If it
isn’t a defined variable, Elixir attempts to call the name as a
function with no arguments. This doesn’t have any special error
handling: a RuntimeError
is raised if the function is undefined:
iex> a ** (RuntimeError) undefined function: a/0 iex> a() ** (RuntimeError) undefined function: a/0
Here’s an example of how barewords are first checked against the variables in the context, then called as a function:
defmodule Example do def five, do: 5 @doc """ Returns the function `five`, 5 in this case, plus `x`. """ def five_plus_fn(x) do five + x end @doc """ Returns the variable `five`, 4 in this case, plus `x`. """ def five_plus_var(x) do five = 4 five + x end end IO.inspect [[:five_plus_fn, Example.five_plus_fn(5)], [:five_plus_var, Example.five_plus_var(5)]]
:five_plus_fn | 10 |
:five_plus_var | 9 |
For Keeping Parens
func()
Unless I have a good reason, I default to using parens because of the clarity it adds to the code.
Clarity
Using parens to call 0 arity functions increases code clarity by explicitly signaling that the reference is a function.
Reading code is a non-linear process of untangling threads of logic until you understand the whole. Having contextual signals (funcall vs var) makes references easier to trace.
The other day, Ryan Johnson was digging around in ExUnit.Case.test/2,3. He asked:
what does `binding` refer to?
Here’s the macro in question, trimmed down for easy digestion:
defmacro test(message, var \\ quote(do: _), contents) do contents = ... var = Macro.escape(var) contents = Macro.escape(contents, unquote: true) quote bind_quoted: binding do test = :"test #{message}" ExUnit.Case.__on_definition__(__ENV__, test) def unquote(test)(unquote(var)), do: unquote(contents) end end
Here’s the process for figuring out what binding
, or any
bareword, refers to:
- Are there any variables bound to
binding
in the context? It’s a bit easier in the example above since I cut out half of the macro at the...
. Answer: no - Are there any 0 arity functions called
binding
defined inExUnit.Case
? Answer: no - Are there any 0 arity functions called
binding
imported byExUnit.Case
?ExUnit.Case
imports four modules, any of which might define the function. Answer: yes - Still can’t find it? Try starting back at (1).
This process is trivial for a computer brain running the correct program, but our human brains are not nearly as well equipped to glean the answer from source code. If you know of any tooling that helps with this, let me know!
After some spelunking, Ryan found did find the answer to his
question in the nifty binding/0 macro. However, even if he’d known
about Kernel.binding/0
, he still would have had to verify that
their are no variables named binding
which supersede the function
call.
If binding()
had been used instead of binding
, you could
scratch step 1: the parens signal that the reference is to a
function rather than a variable. Moreover, if you’re wiser than
Ryan and I and know of Kernel.binding/0
, your job is easy.
In complex code where reference tracing is already difficult, forcing the reader to check through multiple contexts is painful and unnecessary.
For Dropping Parens
func
Dogma has no place in coding style -- here are some situations where it might make sense to drop the parens:
One Name, Multiple Contexts
The bareword duality between variables and function calls allows for easy refactoring from a variable to a function call with no arguments. The follow two code snippets show how most Mixfiles take advantage of this:
defmodule MyApp.Mixfile do def project do deps = [{:ecto, "~> 0.8.0"}] [app: :my_app, deps: deps] end end
The punchline: refactoring out the deps
variable to its own
function without changing the reference to it:
defmodule MyApp.Mixfile do def project do [app: :my_app, deps: deps] end defp deps do [{:ecto, "~> 0.8.0"}] end end
It’s a neat pattern, and works well in simple modules like a
mixfile. I’d avoid using this when the function being called does
significant computation, as opposed to the deps/0
function which
only returns a value.
See Avdi Grimm’s barewords ruby tapas episode for a great description of this.
Piping
Empty trailing parentheses are unnecessary when using the pipe macro.
A simple, contrived example:
1..10 |> Enum.count |> IO.inspect
10
When using piping, there is no ambiguity about what Enum.count
is referring to: it’s being called as function with one
argument. Empty trailing parens provide no additional information.
However, in practice you’ll often find yourself piping through functions which take more than one argument, which muddies the parens aesthetic situation a bit:
1..10 |> Enum.filter(&(rem(&1, 2) == 0)) |> Enum.count |> IO.inspect
5
In sum, dropping parens when piping doesn’t introduce ambiguity for the reader. Use what works best for your situation.
Readability
The argument goes that parantheses are noise, and getting rid of them improves the clarity of your source. But, as argued above, parens are not noise; their presence signals that name refers to a 0-arity function.
As for the scannability of func
vs func()
, I’m not going to
argue aesthetics. Instead, I’ll say that from Prolog to C to
mathematics, trailing parens as notation for a function call is
ubiquitous, arguments or no.
Other
- Discussion of parentheses in function definitions and piping in the elixir style guide.