Avoid Elixir booby traps
I’ll highlight some common ones where, I spent a few minutes looking at the screen, looking like this:
It is pretty helpful to get familiar with error messages because you will see them a lot, especially during the initial days.
Single quotes aren’t strings
In many languages, you are used to using both double and single quotes. They both represent a string. So you will probably do something like:
iex(1)> "123" <> '456'
** (ArgumentError) expected binary argument in <> operator but got: '456'
(elixir 1.12.1) lib/kernel.ex:1893: Kernel.wrap_concatenation/3
(elixir 1.12.1) lib/kernel.ex:1884: Kernel.extract_concatenations/2
(elixir 1.12.1) lib/kernel.ex:1880: Kernel.extract_concatenations/2
(elixir 1.12.1) expanding macro: Kernel.<>/2
iex:11: (file)
iex(2)> '123' <> '456'
** (ArgumentError) expected binary argument in <> operator but got: '123'
(elixir 1.12.1) lib/kernel.ex:1893: Kernel.wrap_concatenation/3
(elixir 1.12.1) lib/kernel.ex:1880: Kernel.extract_concatenations/2
(elixir 1.12.1) expanding macro: Kernel.<>/2
iex:12: (file)
iex(3)> String.reverse('123')
** (FunctionClauseError) no function clause matching in String.reverse/1
The following arguments were given to String.reverse/1:
# 1
'123'
Attempted function clauses (showing 1 out of 1):
def reverse(string) when is_binary(string)
(elixir 1.12.1) lib/string.ex:1585: String.reverse/1
When you have single quotes in Elixir you are using a char list. The name implies what it is, a list of chars.
iex(1)> '123'
'123'
iex(2)> [?1, ?2, ?3]
'123'
iex(3)> ?1
49
iex(4)> is_list('123')
true
It’s a list of integers, more concretely Unicode codepoints.
iex(1)> ?1
49 # codepoint for char 1
A string on the other end is a binary. We are not used to this term related to string, coming from other languages. Since Elixir uses the Erlang VM, you don’t have strings as you are used. Only binary representations of strings.
iex(1)> "123"
"123"
iex(2)> is_binary("123")
The best thing to do is always use double quotes and only resort to single quotes if you need them.
Lists with small integers
iex(39)> [10]
'\n'
iex(40)> [112]
'p'
iex(41)> [10, 112]
'\np'
In mix, Elixir tries to infer the type. And if it quacks like a char list, it will show a char list. If the integer is bigger than the possible codepoints, you won’t see it as a char list because there are no associated code points.
iex(42)> [100000]
[100000]
Changing variables inside if statements
You are used to doing something like this.
x = 1
y = 1
if (x == 1) do
x = 2
y = 3
else
x = 4
y = 5
end
You expect the obvious right?
iex(1)> x
1
iex(2)> y
1
If you declare or change any variable inside an if, unless, cond, case, and similar constructs, the variable, and any change you make, will only be visible inside it.
If you want to change some value inside the if statement, you have to return it.
x = 1
y = 1
{x, y} = if (x == 1) do
x = 2
y = 3
{x, y}
else
x = 4
y = 5
{x, y}
end
iex(1)> x
2
iex(2)> y
3
This is a pretty good language decision. It enables you to refactor your code with more confidence.
Variables rebind when pattern matched
Variables are kind of left-handed. They are very “dynamic”, and change value a lot on the left side of the = and pretty “static” on the right side.
iex(1)> x = 5
1
iex(2)> y = 1
1
iex(3)> {w, y} = {3, x}
{3, 5}
iex(4)> w
3
iex(5)> y
5
You could think that the y would keep its value, but we are on the left side, so it tries to pattern match if possible.
And it’s not only the left side of =. It’s on any place where there pattern matching happens.
y = "1"
x = "2"
case y do
x -> "wtf"
_ -> "can't touch me."
end
The same behavior as when you are on the left side of the =
iex(25)> x = "1"
"1"
iex(26)> y = "2"
"2"
iex(27)> case y do
...(27)> x -> "wtf"
...(27)> _ -> "can't touch me"
...(27)> end
warning: variable "x" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)
iex:28
"wtf"
Check the error message. Elixir kindly says:
Are you sure you didn’t 💩?
If you don’t want the variable to change, use the pin operator ^x
case y do
^x -> "wtf"
_ -> "now it works!"
end
is similar to
case y do
"1" -> "wtf"
_ -> "now it works"
end
Maps aren’t objects
If you use dicts in python you will probably do something like this:
iex(1)> x = %{"show_me" => 987}
%{"show_me" => 987}
iex(2)> x.get("show_me")
** (ArgumentError) you attempted to apply a function on %{"show_me" => 987}. Modules (the first argument of apply) must always be an atom
:erlang.apply(%{"show_me" => 987}, :get, ["show_me"])
There are no objects in Elixir. You don’t have properties. It may seem like properties when you use atoms as keys. But they aren’t properties; they are just keys of maps.
iex(1)> x = %{show_me: 987}
%{show_me: 987}
iex(2)> x.show_me
987
You only do this for keys that already exist on the map, otherwise:
iex(1)> x.something
** (KeyError) key :something not found in: %{show_me: 987}
You need to use a function to get the value of a map, or a list:
iex(1)> x = %{"show_me" => 987}
%{"show_me" => 987}
iex(2)> Map.get(x, "show_me")
987
iex(3)> y = [1, 2, 3]
[1, 2, 3]
iex(4)> Enum.at(y, 2)
3
for is not the for you know
Let’s do a for loop 3 times and change a map’s value.
map = %{x: 0}
for _ <- 1..3 do
map = %{map | x: map.x + 1}
end
Simple enough, let’s try it.
iex(3)> map = %{x: 0}
%{x: 0}
iex(4)> for i <- 1..3 do
...(4)> map = %{map | x: map.x + 1}
...(4)> end
warning: variable "i" is unused (if the variable is not meant to be used, prefix it with an underscore)
iex:4
warning: variable "map" is unused (there is a variable with the same name in the context, use the pin operator (^) to match it or prefix this variable with an underscore if it is not meant to be used)
iex:5
[%{x: 1}, %{x: 1}, %{x: 1}]
iex(5)> map = %{x: 0}
%{x: 0}
iex(6)> for _ <- 1..3 do
...(6)> map = %{map | x: map.x + 1}
...(6)> end
warning: variable "map" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)
iex:7
[%{x: 1}, %{x: 1}, %{x: 1}]
You expect the map to have changed, right? Think again.
It’s lexically scoped inside the do, so its value doesn’t change.
Whenever you want loops to store state, you will need to resource to Enum.reduce or recursion.
Using Enum.reduce
iex(9)> map = %{x: 0}
%{x: 0}
iex(10)> map = 1..3 |> Enum.reduce(map, fn _, acc -> %{acc | x: acc.x + 1} end)
%{x: 3}
Using recursion
defmodule Works do
def with_recursion() do
with_recursion(%{x: 0}, 3)
end
def with_recursion(map, 0), do: map
def with_recursion(map, counter) do
with_recursion(%{map | x: map.x + 1}, counter - 1)
end
end
iex(13)> Works.with_recursion()
%{x: 3}
Updating maps
Now let’s try to change a map value.
iex(14)> looks_like_a_python_dict = %{"x": "1"}
warning: found quoted keyword "x" but the quotes are not required. Note that keywords are always atoms, even when quoted. Similar to atoms, keywords made exclusively of ASCII letters, numbers, and underscores do not require quotes
iex:14
%{x: "1"}
iex(15)> looks_like_a_python_dict["x"] = 1
** (CompileError) iex:15: cannot invoke remote function Access.get/2 inside a match
iex(15)> looks_like_a_python_dict["y"] = 1
** (CompileError) iex:15: cannot invoke remote function Access.get/2 inside a match
This is not an array and not an object.
But there’s the right way to do this in Elixir:
iex(2)> looks_like_a_python_dict = %{x: "1"}
%{x: "1"}
iex(3)> looks_like_a_python_dict = %{looks_like_a_python_dict | x: 1}
%{x: 1}
iex(4)> looks_like_a_python_dict = Map.put(looks_like_a_python_dict, "y", 1)
%{:x => 1, "y" => 1}
strings are not arrays of chars
Whenever you want a specific char of a string you are used to the index syntax:
iex(2)> x_marks_the_spot = ".X......."
".X......."
iex(3)> x_marks_the_spot[1]
** (FunctionClauseError) no function clause matching in Access.get/3
The following arguments were given to Access.get/3:
# 1
".X......."
# 2
1
# 3
nil
Attempted function clauses (showing 5 out of 5):
def get(%module{} = container, key, default)
def get(map, key, default) when is_map(map)
def get(list, key, default) when is_list(list) and is_atom(key)
def get(list, key, _default) when is_list(list)
def get(nil, _key, default)
(elixir 1.12.1) lib/access.ex:283: Access.get/3
Nope, this doesn’t work either.
You have to use the String module to help you out.
iex(2)> x_marks_the_spot = ".X......."
".X......."
iex(3)> x_marks_the_spot |> String.at(1)
"X"
lists are not arrays
When you hear lists, coming from other languages you will think of arrays. So you will probably try something like:
iex(5)> game_scores = [500, 333, 456, 665, 943]
[500, 333, 456, 665, 943]
iex(6)> game_scores[1]
** (ArgumentError) the Access calls for keywords expect the key to be an atom, got: 1
(elixir 1.12.1) lib/access.ex:310: Access.get/3
Forget about it. Say it out loud:
Lists aren’t arrays. They are LINKED LISTS.
Once again, you need to ask Enum for help.
iex(1)> game_scores = [500, 333, 456, 665, 943]
[500, 333, 456, 665, 943]
iex(2)> game_scores |> Enum.at(1)
333
Leaving your object-oriented comfort zone can be challenging. You will have a lot of WTF moments, but slowly things start clicking and making sense. Don’t give up and keep running, or else the object-oriented boulder will catch you again 😱