|
1 | 1 | --- |
2 | 2 | layout: getting_started |
3 | | -title: 17 try, catch and rescue |
| 3 | +title: 17 Comprehensions |
4 | 4 | guide: 17 |
5 | 5 | --- |
6 | 6 |
|
7 | 7 | # {{ page.title }} |
8 | 8 |
|
9 | 9 | {% include toc.html %} |
10 | 10 |
|
11 | | -Elixir has three error mechanisms: errors, throws and exits. In this chapter we will explore each of them and include remarks about when each should be used. |
| 11 | +In Elixir, it is common to loop over an Enumerable, often filtering out some results and mapping values into another list. Comprehensions are syntactic sugar for such constructs: they group those common tasks into the `for` special form. |
12 | 12 |
|
13 | | -## 17.1 Errors |
14 | | - |
15 | | -A sample error can be retrieved by trying to add a number into an atom: |
| 13 | +For example, we can map a list of integers into their squared values: |
16 | 14 |
|
17 | 15 | ```iex |
18 | | -iex> :foo + 1 |
19 | | -** (ArithmeticError) bad argument in arithmetic expression |
20 | | - :erlang.+(:foo, 1) |
| 16 | +iex> for n <- [1, 2, 3, 4], do: n * n |
| 17 | +[1, 4, 9, 16] |
21 | 18 | ``` |
22 | 19 |
|
23 | | -A runtime error can be raised any time by using the `raise/1` macro: |
| 20 | +A comprehension is made of three parts: generators, filters and collectables. |
24 | 21 |
|
25 | | -```iex |
26 | | -iex> raise "oops" |
27 | | -** (RuntimeError) oops |
28 | | -``` |
| 22 | +## 17.1 Generators and filters |
29 | 23 |
|
30 | | -Other errors can be raised with `raise/2` passing the error name and a list of keyword arguments: |
| 24 | +In the expression above, `n <- [1, 2, 3, 4]` is the **generator**. It is literally generating values to be used in the comprehension. Any enumerable can be passed in the right-hand side of the generator expression: |
31 | 25 |
|
32 | 26 | ```iex |
33 | | -iex> raise ArgumentError, message: "invalid argument foo" |
34 | | -** (ArgumentError) invalid argument foo |
| 27 | +iex> for n <- 1..4, do: n * n |
| 28 | +[1, 4, 9, 16] |
35 | 29 | ``` |
36 | 30 |
|
37 | | -You can also define your own errors by creating a module and use the `defexception/1` macro inside it. The most common case is to define an exception with a message field: |
| 31 | +Generator expressions also support pattern matching on their left-hand side; all non-matching patterns are *ignored*. Imagine that, instead of a range, we have a keyword list where the key is the atom `:good` or `:bad` and we only want to compute the square of the `:good` values: |
38 | 32 |
|
39 | 33 | ```iex |
40 | | -iex> defmodule MyError do |
41 | | -iex> defexception message: "default message" |
42 | | -iex> end |
43 | | -iex> raise MyError |
44 | | -** (MyError) default message |
45 | | -iex> raise MyError, message: "custom message" |
46 | | -** (MyError) custom message |
| 34 | +iex> values = [good: 1, good: 2, bad: 3, good: 4] |
| 35 | +iex> for {:good, n} <- values, do: n * n |
| 36 | +[1, 4, 16] |
47 | 37 | ``` |
48 | 38 |
|
49 | | -Exceptions can be rescued by using the `try/rescue` construct: |
| 39 | +Alternatively to pattern matching, filters can be used to filter some particular elements out. For example, we can get filter out all the multiples of 3 and get the square of the remaining values only: |
50 | 40 |
|
51 | 41 | ```iex |
52 | | -iex> try do |
53 | | -...> raise "oops" |
54 | | -...> rescue |
55 | | -...> e in RuntimeError -> e |
56 | | -...> end |
57 | | -%RuntimeError{message: "oops"} |
| 42 | +iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end |
| 43 | +iex> for n <- 0..5, multiple_of_3?.(n), do: n * n |
| 44 | +[0, 9] |
58 | 45 | ``` |
59 | 46 |
|
60 | | -The example above rescues the runtime error and returns the error itself which is then printed in the `iex` session. In practice Elixir developers rarely use the `try/rescue` construct though. For example, many languages would force you to rescue an error when a file cannot open successfully. Elixir instead provides a `File.read/1` function which returns a tuple containing information if the file was opened with success or not: |
| 47 | +Comprehensions filter out all elements for which the filter expression returns `false` or `nil`; all other values are kept. |
61 | 48 |
|
62 | | -```iex |
63 | | -iex> File.read "hello" |
64 | | -{:error, :enoent} |
65 | | -iex> File.write "hello", "world" |
66 | | -:ok |
67 | | -iex> File.read "hello" |
68 | | -{:ok, "world"} |
69 | | -```` |
70 | | -
|
71 | | -There is no `try/rescue` here. In case you want to handle multiple outcomes of opening a file, you can simply use pattern matching with `case`: |
| 49 | +Comprehensions generally provide a much more concise representation than using the equivalent functions from the `Enum` and `Stream` modules. Furthermore, comprehensions also allow multiple generators and filters to be given. Here is an example that receives a list of directories and deletes all files in those directories: |
72 | 50 |
|
73 | | -```iex |
74 | | -iex> case File.read "hello" do |
75 | | -...> {:ok, body} -> IO.puts "got ok" |
76 | | -...> {:error, body} -> IO.puts "got error" |
77 | | -...> end |
| 51 | +```elixir |
| 52 | +for dir <- dirs, |
| 53 | + file <- File.ls!(dir), |
| 54 | + path = Path.join(dir, file), |
| 55 | + File.regular?(path) do |
| 56 | + File.rm!(path) |
| 57 | +end |
78 | 58 | ``` |
79 | 59 |
|
80 | | -At the end of the day, it is up to your application to decide if an error while opening a file is exceptional or not. That's why Elixir doesn't impose exceptions on `File.read/1` and many other functions. Instead we leave it up to the developer to choose the best way to proceed. |
| 60 | +Keep in mind that variable assignments inside the comprehension, be it in generators, filters or inside the block, are not reflected outside of the comprehension. |
81 | 61 |
|
82 | | -For the cases where you do expect a file to exist (and the lack of a file is truly an error) you can simply use `File.read!/1`: |
| 62 | +## 17.2 Bitstring generators |
83 | 63 |
|
84 | | -```iex |
85 | | -iex> File.read! "unknown" |
86 | | -** (File.Error) could not read file unknown: no such file or directory |
87 | | - (elixir) lib/file.ex:305: File.read!/1 |
88 | | -``` |
89 | | - |
90 | | -In other words, we avoid using `try/rescue` because **we don't use errors for control flow**. In Elixir, we take errors literally: they are reserved to unexpected and/or exceptional situations. In case you actually need flow control constructs, throws must be used. That's what we are going to see next. |
91 | | - |
92 | | -## 17.2 Throws |
93 | | - |
94 | | -In Elixir, one can throw a value to be caught later. `throw` and `catch` are reserved for situations where it is not possible to retrieve a value unless by using `throw` and `catch`. |
95 | | - |
96 | | -Those situations are quite uncommon in practice unless when interfacing with a library that does not provide the proper APIs. For example, let's imagine the `Enum` module did not provide any API for finding a value and we need to find the first number that is a multiple of 13: |
| 64 | +Bitstring generators are also supported and are very useful when you need to comprehend over bitstring streams. The example below receives a list of pixels from a binary with their respective red, green and blue values and converts them into tuples of three elements each: |
97 | 65 |
|
98 | 66 | ```iex |
99 | | -iex> try do |
100 | | -...> Enum.each -50..50, fn(x) -> |
101 | | -...> if rem(x, 13) == 0, do: throw(x) |
102 | | -...> end |
103 | | -...> "Got nothing" |
104 | | -...> catch |
105 | | -...> x -> "Got #{x}" |
106 | | -...> end |
107 | | -"Got -39" |
| 67 | +iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>> |
| 68 | +iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b} |
| 69 | +[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}] |
108 | 70 | ``` |
109 | 71 |
|
110 | | -However, in practice one can simply use `Enum.find/2`: |
| 72 | +A bitstring generator can be mixed with the "regular" enumerable generators and provides filters as well. |
111 | 73 |
|
112 | | -```iex |
113 | | -iex> Enum.find -50..50, &(rem(&1, 13) == 0) |
114 | | --39 |
115 | | -``` |
116 | | - |
117 | | -## 17.3 Exits |
118 | | - |
119 | | -Every Elixir code runs inside processes that communicates with each other. When a process dies, it sends an `exit` signal. A process can also die by explicitly sending an exit signal: |
120 | | - |
121 | | -```iex |
122 | | -iex> spawn_link fn -> exit(1) end |
123 | | -#PID<0.56.0> |
124 | | -** (EXIT from #PID<0.56.0>) 1 |
125 | | -``` |
| 74 | +## 17.3 Results other than lists |
126 | 75 |
|
127 | | -In the example above, the linked process died by sending an `exit` signal with value of 1. The Elixir shell automatically handles those messages and prints them to the terminal. |
| 76 | +In the examples above, all the comprehensions returned lists as their result. However, the result of a comprehension can be inserted into different data structures by passing the `:into` option to the comprehension. |
128 | 77 |
|
129 | | -`exit` can also be "caught" using `try/catch`: |
| 78 | +For example, a bitstring generator can be used with the `:into` option in order to easily remove all spaces in a string: |
130 | 79 |
|
131 | 80 | ```iex |
132 | | -iex> try do |
133 | | -...> exit "I am exiting" |
134 | | -...> catch |
135 | | -...> :exit, _ -> "not really" |
136 | | -...> end |
137 | | -"not really" |
| 81 | +iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>> |
| 82 | +"helloworld" |
138 | 83 | ``` |
139 | 84 |
|
140 | | -Using `try/catch` is already uncommon and using it to catch exits is even more rare. |
141 | | - |
142 | | -`exit` signals are an important part of the fault tolerant system provided by the Erlang VM. Processes usually run under supervision trees which are themselves processes that just wait for `exit` signals of the supervised processes. Once an exit signal is received, the supervision strategy kicks in and the supervised process is restarted. |
143 | | - |
144 | | -It is exactly this supervision system that makes constructs like `try/catch` and `try/rescue` so uncommon in Elixir. Instead of rescuing a certain error, we'd rather "fail fast" since the supervision tree will guarantee our application will go back to a known initial state after the error. |
145 | | - |
146 | | -## 17.4 After |
| 85 | +Sets, maps and other dictionaries can also be given to the `:into` option. In general, `:into` accepts any structure that implements the `Collectable` protocol. |
147 | 86 |
|
148 | | -Sometimes it is necessary to use `try/after` to guarantee a resource is cleaned up after some particular action. For example, we can open a file and guarantee it is closed with `try/after` block: |
| 87 | +A common use case of `:into` can be transforming values in a map, without touching the keys: |
149 | 88 |
|
150 | 89 | ```iex |
151 | | -iex> {:ok, file} = File.open "sample", [:utf8, :write] |
152 | | -iex> try do |
153 | | -...> IO.write file, "olá" |
154 | | -...> raise "oops, something went wrong" |
155 | | -...> after |
156 | | -...> File.close(file) |
157 | | -...> end |
158 | | -** (RuntimeError) oops, something went wrong |
| 90 | +iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val} |
| 91 | +%{"a" => 1, "b" => 4} |
159 | 92 | ``` |
160 | 93 |
|
161 | | -## 17.5 Variables scope |
162 | | - |
163 | | -It is important to bear in mind that variables defined inside `try/catch/rescue/after` blocks do not leak to the outer context. This is because the `try` block may fail and as such the variables may never be bound in the first place. In other words, this code is invalid: |
| 94 | +Let's make another example using streams. Since the `IO` module provides streams (that are both `Enumerable`s and `Collectable`s), an echo terminal that echoes back the upcased version of whatever is typed can be implemented using comprehensions: |
164 | 95 |
|
165 | 96 | ```iex |
166 | | -iex> try do |
167 | | -...> from_try = true |
168 | | -...> after |
169 | | -...> from_after = true |
| 97 | +iex> stream = IO.stream(:stdio, :line) |
| 98 | +iex> for line <- stream, into: stream do |
| 99 | +...> String.upcase(line) <> "\n" |
170 | 100 | ...> end |
171 | | -iex> from_try |
172 | | -** (RuntimeError) undefined function: from_try/0 |
173 | | -iex> from_after |
174 | | -** (RuntimeError) undefined function: from_after/0 |
175 | 101 | ``` |
176 | 102 |
|
177 | | -This finishes our introduction to `try`, `catch` and `rescue`. You will find they are used less frequently in Elixir than in other languages although they may be handy in some situations where a library or some particular code is not playing "by the rules". |
178 | | - |
179 | | -It is time to talk about some Elixir constructs like comprehensions and sigils. |
| 103 | +Now type any string into the terminal and you will see that the same value will be printed in upper-case. Unfortunately, this example also got your IEx shell stuck in the comprehension, so you will need to hit `Ctrl+C` twice to get out of it. :) |
0 commit comments