- Elixir
- Debug in IEx
- Inspect failing tests
- Create custom sigils
- Get a value from nested maps
- Get all values for a given map key in a list
- Arithmetic operators as lambda functions
- Pattern match [ vs ] destructure
- Data Comprehension with filters
- Comprehension with binary strings
- Replace keys in map
- Group things
- Find N slowest tests
- Custom inspection for structure
- Querying Ecto for a subset of the fields along with relation info
This is a notebook for all the useful info I can find or read about Elixir.
[2020-03-20 Fri 20:51]
break!
is the way to go when you want to set a break point in your code from within IEx:
iex(1)> break!(MyModule.my_func/1)
iex(1)> break!(MyModule, :my_func, 1)
You'll often want to break on a recursive function in which case you can pass an additional argument specifying how many time you want to stop in this function:
break!(MyModule, :my_func, 1, 10)
You can also list all breakpoints:
iex(1)> breaks()
ID Module.function/arity Pending stops
---- ----------------------- ---------------
1 MyModule.my_func/1 1
Once a break point is set you can go through your code and inspect it:
iex(2)> MyModule.my_func("args")
Break reached: MyModule.my_func/1 (lib/my_module.ex:2)
1: defmodule MyModule do
2: def my_func(args) do
3: "#{args}"
4: end
pry(1)> args
"args"
When you're done with debugging you can resume the execution by cleaning break points:
pry(2)> respawn
[2020-03-20 Fri 20:53]
Sometimes it can be handy to inspect our failing test using a debugger. Elixir covers our back by allowing us to use IEx.pry
in tests:
require IEx; IEx.pry
You can then run iex -S mix test
with the --trace
option:
iex -S mix test --trace path/to/test.exs
[2020-03-20 Fri 21:14]
Sigils calls a function to know how to act.
You can define a sigil this way:
defmodule MySigils do
# returns the downcased string, if option l is given then returns the list of downcased letters
def sigil_l(string,[]), do: String.downcase(string)
def sigil_l(string,[?l]), do: String.downcase(string) |> String.graphemes
# returns the upcased string, if option l is given then returns the list of downcased letters
def sigil_u(string,[]), do: String.upcase(string)
def sigil_u(string,[?l]), do: String.upcase(string) |> String.graphemes
end
[2020-03-20 Fri 21:16]
The get_in
function can be used to retrieve a nested value in nested maps using a list of keys:
nested_map = %{name: %{first_name: "nico"}} # Example of Nested Map
first_name = get_in(nested_map, [:name, :first_name]) # Retrieving the Key
It will return nil
if the key is missing.
[2020-03-21 Sat 16:47]
You can also use get_in
in conjunction with Access.all/0
to get all the values in a map for a given key:
iex> users = [%{"user" => %{"first_name" => "john", "age" => 23}},
%{"user" => %{"first_name" => "hari", "age" => 22}},
%{"user" => %{"first_name" => "mahesh", "age" => 21}}]
iex> get_in(users, [Access.all(), "user", "age"])
[23, 22, 21]
iex> get_in(users, [Access.all(), "user", "first_name"])
["john", "hari", "mahesh"]
You can use the same technique to update all the values for a given key:
iex> user = %{name: "john", books: [%{name: "my soul", type: "tragedy"},
%{name: "my heart", type: "romantic"},
%{name: "my enemy", type: "horror"}]}
iex> update_in(user, [:books, Access.all(), :name], &String.upcase/1)
%{books: [%{name: "MY SOUL", type: "tragedy"}, %{name: "MY HEART", type: "romantic"}, %{name: "MY ENEMY", type: "horror"}], name: "john"}
[2020-03-20 Fri 21:24]
In Elixir every operator is a macro. It makes it possible to use them as them as lambda functions:
iex> Enum.reduce([1,2,3], 0, &+/2)
6
iex> Enum.reduce([1,2,3], 0, &*/2)
0
iex> Enum.reduce([1,2,3], 3, &*/2)
18
iex> Enum.reduce([1,2,3], 3, &-/2)
-1
iex> Enum.reduce([1,2,3], 3, &//2)
0.5
[2020-03-20 Fri]
=
is dedicated to pattern matching but you cannot do [a, b, c] = [1, 2, 3, 4] because the terms count is different so it will raise a MatchError
.
iex(11)> [a, b, c] = [1, 2, 3, 4]
(MatchError) no match of right hand side value: [1, 2, 3, 4]
If you want to do that, you have to use destructure/2
:
iex(1)> destructure([a, b, c], [1, 2, 3, 4])
[1, 2, 3]
iex(2)> {a, b, c}
{1, 2, 3}
iex> destructure([a, b, c], [1])
iex> {a, b, c}
{1, nil, nil}
[2020-03-21 Sat 16:54]
When using a for
loop (a comprehension), you can add conditions to filter values used:
iex> for x <- [1, 2, 3, 4],
y <- [5, 6, 7, 8],
rem(x * y, 2) == 0,
do: {x, y, x * y}
[{1, 5, 5}, {1, 7, 7}, {3, 5, 15}, {3, 7, 21}]
In this example rem(x * y, 2) == 0
is acting as a filter, so tuples that are not matching the condition aren't processed.
[2020-03-21 Sat 16:55]
You can also use comprehension with strings using bitstring notation:
iex> string = "Bounga"
"Bounga"
iex> for << x <- string >>, do: x + 1
'Cpvohb'
[2020-03-21 Sat 22:43]
Sometimes you'll have to modify keys for a provided map. Most of the time that's because you're getting a map out of an API call and you want to change key names for your internal use:
iex> location = %{latitude: 38.8951, longitude: -77.0364}
iex> Enum.into(location, %{}, fn
{:latitude, lat} -> {:lat, lat}
{:longitude, long} -> {:long, long}
end)
%{lat: 38.8951, long: -77.0364}
BTW you can also use Enum.into/2
to simply merge things:
Enum.into([1, 2], [])
[1, 2]
iex> Enum.into([a: 1, b: 2], %{})
%{a: 1, b: 2}
iex> Enum.into(%{a: 1}, %{b: 2})
%{a: 1, b: 2}
iex> Enum.into([a: 1, a: 2], %{})
%{a: 2}
[2020-03-21 Sat 23:55]
Let say you have a list of structs or well-defined maps and you want to group them by a given key value, you'll use Enum.group_by/2
:
iex> hotel_bookings = [
%{name: "John", country: "usa", booking_status: :success},
%{name: "Hari", country: "india", booking_status: :fail},
%{name: "Mahesh", country: "austria", booking_status: :pending},
%{name: "Ruchi", country: "paris", booking_status: :success},
%{name: "Nitesh", country: "malasia", booking_status: :success},
%{name: "Manoj", country: "japan", booking_status: :fail},
%{name: "Akhilesh", country: "china", booking_status: :pending},
%{name: "Rajesh", country: "india", booking_status: :fail},
%{name: "Payal", country: "london", booking_status: :success},
%{name: "Kumar", country: "france", booking_status: :fail}
]
iex> Enum.group_by(hotel_bookings, &Map.get(&1, :booking_status))
%{
fail: [
%{booking_status: :fail, country: "india", name: "Hari"},
%{booking_status: :fail, country: "japan", name: "Manoj"},
%{booking_status: :fail, country: "india", name: "Rajesh"},
%{booking_status: :fail, country: "france", name: "Kumar"}
],
pending: [
%{booking_status: :pending, country: "austria", name: "Mahesh"},
%{booking_status: :pending, country: "china", name: "Akhilesh"}
],
success: [
%{booking_status: :success, country: "usa", name: "John"},
%{booking_status: :success, country: "paris", name: "Ruchi"},
%{booking_status: :success, country: "malasia", name: "Nitesh"},
%{booking_status: :success, country: "london", name: "Payal"}
]
}
You can also go further and get a list one of a key value only (here we'll extract the names) in groups rather than the whole map:
iex> Enum.group_by(hotel_bookings, &Map.get(&1, :booking_status), &Map.get(&1, :name))
%{
fail: ["Hari", "Manoj", "Rajesh", "Kumar"],
pending: ["Mahesh", "Akhilesh"],
success: ["John", "Ruchi", "Nitesh", "Payal"]
}
[2020-03-22 Sun 00:06]
It can be useful to find out what are your slowest tests in your test suite so you can act accordingly, Mix
helps with that:
mix test --slowest 3
will show us the three slowest tests.
[2020-03-22 Sun 00:14]
When you're examining your code, you'll often find yourself using IO.inspect
to get info about your structure.
In Elixir, when you define a structure you can also declare its implementation for the Inspect
module:
defmodule Student do
defstruct name: "John", place: "Earth"
end
defimpl Inspect, for: Student do
def inspect(student, _opts) do
"""
-----------|---------------------
Name : #{student.name}
-----------|---------------------
Place : #{student.place}
-----------|---------------------
"""
end
end
iex> %Student{}
-----------|---------------------
Name : John
-----------|---------------------
Place : Earth
-----------|---------------------
When querying your database you often don't need to retrieve all the data for each row, you use-case may only need a subset of it.
Ecto provides a clean way to do that even if you need to also retrieve relation computed info along in a single query:
query =
from u in "users",
join: c in "comments",
on: c.user_id == u.id,
where: u.active == true,
group_by: [u.name, u.email],
select: %{name: u.name, email: u.email, comments_count: count(c.id)}
Repo.all(query)
# => [
%{comments_count: 23, email: "[email protected]", name: "Gandalf"},
%{comments_count: 45, email: "[email protected]", name: "Aragorn"},
%{comments_count: 566, email: "[email protected]", name: "Gimli"},
%{...},
...
]