Common Gotchas For Elixir Newbies
Elixir was my first functional programming language (unless you count JavaScript). Mostly, my experience was with object-oriented languages like Ruby (and a bit of C++ and Java back in the day).
So when I started Elixir, there were new concepts that I ran into as I was writing code that produced unexpected results. Here is my attempt to recall all the gotchas I ran into.
Gotcha 1 – Pattern matching – top down order
One of the first exciting concepts I ran into was pattern matching in Elixir. Basically, pattern matching means you call the function that “matches” both the shape of the data structure and/or the value of the arguments you pass in.
What I didn’t realize was that Elixir executes pattern matching from the top down. Let’s take a look at some code.
defmodule M do
def m(x), do: "hello #{x}"
def m(nil), do: "found nil"
def m(%{a: "hello"} = t), do: "found #{Map.get(t, :a)}"
end
What do you think calling M.m(nil) does? Below is the result.
M.m(nil)
#=> returns "hello " instead of "found nil"
What happened? Well Elixir matched the “nil” on the “x” variable in m(x) first, so it executed that function.
Now if I swap the order of m(x) and m(nil), my expectations are met.
defmodule M do
def m(nil), do: "found nil"
def m(x), do: "hello #{x}"
def m(%{a: "hello"} = t), do: "found #{Map.get(t, :a)}"
end
Finally, I get “found nil”.
M.m(nil)
#=> returns "found nil"
Gotcha 2 – The double backslash in function parameters
When I first got to Elixir, I noticed functions like this:
defmodule M do
def default(x \\ 2), do: x + 4
end
What are those double backslashes? As it turns out, they specify default values for arguments.
iex(5)> M.default
#=> returns 6
iex(6)> M.default(3)
#=> returns 7
Gotcha 3 – Piping to an anonymous function
Below I’m passing a list into Enum.any/2 and checking if any of the values are equal to 3.
You’ll notice I’m using the “&” operator to define an anonymous function to check equality on the values. If we were to expand that function it would read fn(x) -> x end.
[2,3]
|> Enum.any?(&(&1 == 3))
Gotcha 4 – Piping an argument that is not the first argument to a function embedded as an anonymous function argument
When you want to pipe an argument that is not the first argument to a function embedded as an anonymous function argument, you have to use the capture (“&”) operator.
Here is the module M redefined.
defmodule M do
def mdiv(x, y), do: x / y
end
Suppose we want to map over “M.mdiv”. Since Enum.map/2 takes 2 arguments, one a list and the other an anonymous function, we use the capture operator as follows.
[2, 2]
|> Enum.map(&(M.mdiv(3, &1)))
#=> [1.5, 1.5]
Gotcha 5 – Piping an argument that is not the first argument to a function
I talked about this in The Elixir Pipe and Capture Operators in Case 2: Piping an argument that is not the first argument to another function, but I think it’s worth reiterating.
defmodule M do
def mdiv(x, y), do: x / y
end
2
|> (&(M.mdiv(3, &1))).()
1.5
You’ll notice that if you don’t put the “.()” at the end of the function call, you’ll get an error message like this:
** (ArgumentError) cannot pipe 2 into &(M.mdiv(3, &1)), can only pipe into local calls foo(), remote calls Foo.bar() or anonymous functions calls foo.()
(elixir) lib/macro.ex:118: Macro.pipe/3
(stdlib) lists.erl:1263: :lists.foldl/3
(elixir) expanding macro: Kernel.|>/2
iex:4: (file)
Gotcha 6 – Nested case smells
I wrote about using the with statement to deal with the nested case smell but it’s worth bringing up the nested case as a gotcha.
defmodule Airplane do
def undo_jet_engine_start(params) do
case get_engine_part_by_diameter(params["diameter"]) do
{:ok, engine} ->
changeset = Engine.engine_changeset(engine,
%{
is_turbine_engine: false,
})
case Repo.update(changeset) do
{:ok, t} ->
{:ok, t}
{:error, changeset} ->
{:error, changeset}
end
{:error, error} -> {:error, error}
end
end
def get_engine_part_by_diameter(diameter), do: {:ok, "demo"}
end
As I said in the post on untangling nested case using the with statement, using nested case makes it harder to read your code and hence, harder to debug and maintain. You can dig yourself out of the nested case pattern with a “with statement”.
Gotcha 7 – Using cond instead of pattern matching or case
When I was making nested_filter, I was pretty new to Elixir and still getting the hang of things.
I wrote a function like this:
defp is_nested_map?(map) do
cond do
is_map(map) ->
map
|> Enum.any?(fn{key, _} -> is_map(map[key]) end)
true ->
false
end
end
Thanks to scohen’s comment on this issue of nested_filter, I learned that pattern matching is in generally preferable to using a “cond” statement. Also, using a case statement might be more preferable as well.
Gotcha 8 – No custom functions in guards
This is something you can end up learning the hard way. I talk about this in detail in the section “Why You Can’t Use Custom Functions With Guards” of the Ultimate Guide to Elixir.
Here’s an example of a function with a guard clause:
defmodule M do
def print_list(list) when is_list(list), do: IO.inspect list
end
You’ll notice the is_list/1 function is a built-in function. Basically, we’ll only call print_list/1 when the argument passed to it is a list. Due to Elixir’s rules, you cannot have your own custom (i.e., you can’t use a non-built-in function) function as part of the guard clause.
Gotcha 9 – You don’t assign, you bind
One item of note that might seem rather pedantic, but when you see an expression such as the following, you are doing something called “binding”.
x = 2
This is actually a case of a successful pattern match.
As this poster says in the Elixir forum1 states:
The compiler/runtime has a set of rules that it uses to attempt to make the pattern match succeed. One of these is “binding” and “re-binding”. Binding creates a variable that references a term in memory, if that variable does not already exist in the current scope. In Erlang, all further uses of that variable in it’s current scope are fixed to that term in memory. Elixir cheats a bit and allows you to “re-bind”[2] a variable to a new term in memory in certain situations, however the actual term in memory from the original binding does not change and will need to be garbage collected at some point.
As a side note, the Platformatec blog has an interesting article on comparing Elixir and Erlang variables and talks a bit about the concept of “rebinding”.
Gotcha 10 – A for is a comprehension, not a loop
If you’re an object-oriented programmer with a background in something like Java, Ruby, C++, or JavaScript, you might see something like the below and think you’ve discovered a “for loop” in Elixir.
for x <- [6, 7, 8], do: 2*x
Nothing could be further from the truth. As I discuss in the comprehensions section of the Ultimate Elixir Guide, comprehensions are basically “syntactic sugar” for iterating over a list and tranforming the data in that list to something else.