Skip to content

Commit 2a4acd7

Browse files
committed
Move the try/catch/rescue chapter after the sigils one
The chapter about try/catch/rescue, which was chapter 17 before this commit, has been moved to chapter 19, and the chapters about comprehensions and sigils (18 and 19 respectively) have been scaled down (17 and 18 now).
1 parent e44e57f commit 2a4acd7

File tree

4 files changed

+301
-301
lines changed

4 files changed

+301
-301
lines changed

_layouts/getting_started.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ <h3 class="widget-title">Getting Started</h3>
2626
<li><a class="spec" href="/getting_started/14.html">Module attributes</a></li>
2727
<li><a class="spec" href="/getting_started/15.html">Structs</a></li>
2828
<li><a class="spec" href="/getting_started/16.html">Protocols</a></li>
29-
<li><a class="spec" href="/getting_started/17.html">try, catch and rescue</a></li>
30-
<li><a class="spec" href="/getting_started/18.html">Comprehensions</a></li>
31-
<li><a class="spec" href="/getting_started/19.html">Sigils</a></li>
29+
<li><a class="spec" href="/getting_started/17.html">Comprehensions</a></li>
30+
<li><a class="spec" href="/getting_started/18.html">Sigils</a></li>
31+
<li><a class="spec" href="/getting_started/19.html">try, catch and rescue</a></li>
3232
<li><a class="spec" href="/getting_started/20.html">Typespecs and behaviours</a></li>
3333
<li><a class="spec" href="/getting_started/21.html">Where to go next</a></li>
3434
</ol>

getting_started/17.markdown

Lines changed: 48 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,179 +1,103 @@
11
---
22
layout: getting_started
3-
title: 17 try, catch and rescue
3+
title: 17 Comprehensions
44
guide: 17
55
---
66

77
# {{ page.title }}
88

99
{% include toc.html %}
1010

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.
1212

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:
1614

1715
```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]
2118
```
2219

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.
2421

25-
```iex
26-
iex> raise "oops"
27-
** (RuntimeError) oops
28-
```
22+
## 17.1 Generators and filters
2923

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:
3125

3226
```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]
3529
```
3630

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:
3832

3933
```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]
4737
```
4838

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:
5040

5141
```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]
5845
```
5946

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.
6148

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:
7250

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
7858
```
7959

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.
8161

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
8363

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:
9765

9866
```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}]
10870
```
10971

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.
11173

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
12675

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.
12877

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:
13079

13180
```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"
13883
```
13984

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.
14786

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:
14988

15089
```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}
15992
```
16093

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:
16495

16596
```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"
170100
...> end
171-
iex> from_try
172-
** (RuntimeError) undefined function: from_try/0
173-
iex> from_after
174-
** (RuntimeError) undefined function: from_after/0
175101
```
176102

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

Comments
 (0)