Skip to content

Commit af3b3bd

Browse files
committed
3 Elixir и паттернматчинг
1 parent ec49e4e commit af3b3bd

4 files changed

Lines changed: 260 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@
1313
* [PostgresSQL пример построения отчета в маленьком магазине](/posts/sql_lesson.md)
1414
* [Графы, немного теории и знакомство с графовой СУБД Neo4j](/posts/neo4j_and_graphs.md)
1515

16+
## Elixir
17+
18+
* [Elixir и паттернматчинг](/posts/elixir_patternmatching.md)
19+
30.7 KB
Loading
21.5 KB
Loading

posts/elixir_patternmatching.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
2+
# Elixir и паттернматчинг
3+
4+
5+
В Эликсире много возможностей для эффективного написания кода. Новички, осваивая язык, проникаются его различными возможностями и начинают их применять повсеместно.
6+
7+
В этой статье речь пойдет про **перегрузку функций с паттернматчингом аргументов**
8+
9+
Эликсир позволяет осуществлять перегрузку функций, как, например, Java. Достаточно объявить две одноименные функции в одном модуле, например, рекурсивный обход коллекции с умножением каждого элемента на 2.
10+
11+
```elixir
12+
defmodule Math do
13+
def double_each([head | tail]) do
14+
[head * 2 | double_each(tail)]
15+
end
16+
17+
def double_each([]) do
18+
[]
19+
end
20+
end
21+
```
22+
23+
Разберем еще один пример: создадим функцию, которая сортирует **разные типы данных**, в данном случае — **list **и** tuple.**
24+
25+
```elixir
26+
defmodule Utils do
27+
def sort(data) when is_list(data) do
28+
data
29+
|> Enum.sort()
30+
end
31+
32+
def sort(data) when is_tuple(data) do
33+
data
34+
|> Tuple.to_list()
35+
|> sort()
36+
|> List.to_tuple()
37+
end
38+
end
39+
```
40+
41+
Я не просто так выделил фразу “разные типы данных”, ведь в первую очередь перегрузка функций появилась в типизированных языках и используется при передаче аргументов разных типов или разном количестве аргументов.
42+
43+
Это нормальные случаи использования перегрузки функции и паттернматчинга.
44+
45+
## Теперь я хочу показать несколько примеров из реальных проектов, в которых паттернматчинг и перегрузка функций использовались во зло.
46+
47+
### Пример №1
48+
49+
Модуль Helper, который в зависимости от контекста и переданного кода ошибки возвращает текстовое описание:
50+
51+
```elixir
52+
defmodule ExternalApi.ErrorHelper do
53+
@errors_grouped_by_action %{...}
54+
@unknown_error_message "unknown"
55+
56+
def get_description(:email_sending, status_code) do
57+
@errors_grouped_by_action
58+
|> Map.get(:email_sending)
59+
|> Map.get(status_code, @unknown_error_message)
60+
end
61+
def get_description(:put_user_info, status_code) do
62+
@errors_grouped_by_action
63+
|> Map.get(:put_user_info)
64+
|> Map.get(status_code, @unknown_error_message)
65+
end
66+
def get_description(:get_user_info, status_code) do
67+
@errors_grouped_by_action
68+
|> Map.get(:get_user_info)
69+
|> Map.get(status_code, @unknown_error_message)
70+
end
71+
end
72+
```
73+
74+
**В таком коде есть пара проблем:**
75+
1. Между определениями функций нет пустой строки — всё слилось в однонепонятночто ;)
76+
2. При необходимости расширения придется добавлять копипасту функции;
77+
78+
### **Рефакторинг примера**
79+
80+
```elixir
81+
defmodule ExternalApi.ErrorHelper do
82+
@error_status_codes %{...}
83+
@unknown_error_message "unknown"
84+
85+
def get_description(action_name, status_code) do
86+
@error_status_codes
87+
|> Map.get(action_name)
88+
|> Map.get(status_code, @unknown_error_message)
89+
end
90+
end
91+
```
92+
93+
Вместо трёх функций у нас теперь одна, которая выполняет все те же функции, сосредотачивает всю логику в одном месте, выглядит просто и элегантно. Если необходимо, чтобы в случае передачи несуществующего **action_name** возвращалась ошибка — достаточно добавить **guard**, и перечислить все возможные значения, например:
94+
95+
```elixir
96+
def get_description(action_name, status_code) when action_name in [:email_sending, :put_user_info, :get_user_info] do
97+
...
98+
end
99+
```
100+
101+
### Пример №2
102+
103+
Есть сервис [Cbr-xml-daily](https://www.cbr-xml-daily.ru/), дающий API для получения курсов валют ЦБ РФ. Соответственно все курсы валют представлены по отношению к рублю.
104+
105+
**Пример полученных данных по курсу валют:**
106+
> Формат: {Номинал} {Символ Валюты} = {Сумма в рублях}р
107+
> 1 USD = 68.95р
108+
1 EUR = 79.32р
109+
100 JPY = 61.21р
110+
> и т.п.
111+
> В итоговом хеше ключ — символ валюты, значение — тоже хеш, где value — сумма в рублях, nominal — номинал
112+
113+
```elixir
114+
%{
115+
usd: %{nominal: 1, value: 68.95},
116+
eur: %{nominal: 1, value: 79.32},
117+
jpy: %{nominal: 100, value: 61.21},
118+
...
119+
}
120+
```
121+
122+
В одном проекте есть модуль, который переводит сумму из валюты в рубли по полученному курсу.
123+
124+
```elixir
125+
defmodule Currency do
126+
# в данном примере courses - это map-структура, описанная выше.
127+
# courses => %{usd: %{nominal: 1, value: 68.95}, eur: %{...}}
128+
129+
def exchange(_courses, amount, :rub, :rub) do
130+
amount
131+
end
132+
133+
def exchange(courses, amount, from, :rub) do
134+
%{nominal: nominal, value: value} = courses[from]
135+
(amount * nominal) * value
136+
end
137+
138+
def exchange(courses, amount, :rub, to) do
139+
%{nominal: nominal, value: value} = courses[to]
140+
(amount * nominal) / value
141+
end
142+
143+
def exchange(courses, amount, from, to) do
144+
amount_in_rub = exchange(courses, amount, from, :rub)
145+
exchange(courses, amount_in_rub, :rub, to)
146+
end
147+
end
148+
```
149+
150+
Обилие перегруженных с паттернатчингом функций “размазывает” логику на несколько реализаций, и, честно говоря, я не понимаю, как это все вместе работает. Но из названия ясно, что функция должна выполнять, это и стало толчком к рефакторингу.
151+
152+
**Рефакторинг примера**
153+
154+
На самом деле упрощенный вариант не так уж и прост, вся логика находится в одной функции, но усложнился алгоритм подсчета. Для коллег такая алгебра может показаться сложной, поэтому стоит добавить комментарий, описывающий алгоритм.
155+
156+
```elixir
157+
defmodule Currency do
158+
159+
def exchange(courses, amount, from, to) do
160+
{nominal_from, rub_from} = get_nominal_and_value(courses, from)
161+
{nominal_to, rub_to} = get_nominal_and_value(courses, to)
162+
163+
# Когда получили соотношения валют к рублю,
164+
# надо получить их соотношения друг к другу и умножить
165+
# на количество исходной валюты.
166+
167+
amount * (rub_from / nominal_from) / (rub_to / nominal_to)
168+
end
169+
end
170+
```
171+
172+
**Пример использования: **переведем 10 долларов США в бразильские реалы
173+
174+
```elixir
175+
CurrencyExchange.exchange(courses, 10, :usd, :brl)
176+
# => 40.20
177+
```
178+
179+
**Пример описания алгоритма:** Так как все курсы валют представлены по отношению к рублю, то нам неизвестно отношение доллара к евро. Но мы можем это отношение высчитать, через рубли. Для корректной конвертации надо получить соотношение каждой валюты к рублю. Для этого надо поделить курс валюты на ее номинал.
180+
Когда получили соотношения валют к рублю, надо получить их соотношения друг к другу и умножить на количество исходной валюты.
181+
182+
## Хочу привести еще один пример, когда не стоит использовать паттернматчинг с перегрузкой функций.
183+
184+
**Давайте решим задачу:**
185+
186+
Необходимо реализовать геометрический модуль, благодаря которому можно вычислять площади следующих фигур: круг, прямоугольник и квадрат.
187+
188+
Задача звучит просто, и вы, возможно, сразу захотите написать модуль, в котором будет одна функция с несколькими реализациями. Давайте попробуем:
189+
190+
191+
```elixir
192+
defmodule Geometry do
193+
# Площадь круга с радиусом r равна πr2.
194+
def square(:cirle, radius) do
195+
pi = 3.14
196+
pi * radius * radius
197+
end
198+
199+
# Площадь квадрата равна квадрату длины его стороны.
200+
def square(:square, length) do
201+
length * length
202+
end
203+
204+
# Площадь прямоугольника равна произведению его длины и ширины
205+
def square(:rectangle, height, width) do
206+
height * width
207+
end
208+
end
209+
```
210+
211+
Все работает, проверять не будем. Перейдем к **проблемам:**
212+
213+
1. Автодополнение не может подсказать, какие у нас есть реализации подсчета площади. Так же отображается 2 функции, вместо трех, из-за различного количества аргументов. Если бы у всех трех функций была бы одинаковая арность — отобразилась бы 1 функция;
214+
215+
![](/images/elixir_patternamtching:autocomplete_example.png)
216+
217+
2. Проблема вытекает из первой, необходимо смотреть реализацию, чтобы понять, площади каких фигур умеет считать модуль.
218+
219+
**Альтернативное решение**
220+
221+
Избавимся от паттерматчинга и перегрузки функций. Название функции должно отображать суть, абстрагировать нас от реализации и помогать писать программы, чтобы мы не тратили время на изучение сторонних фукнций и как они устроены.
222+
223+
Поэтому мы дадим функциям явные названия, которые будут отражать суть — площадь какой фигуры находим.
224+
225+
```elixir
226+
defmodule Geometry do
227+
# Площадь круга с радиусом r равна πr2.
228+
def circle_area(radius) do
229+
pi = 3.14
230+
pi * radius * radius
231+
end
232+
233+
# Площадь квадрата равна квадрату длины его стороны.
234+
def square_area(length) do
235+
length * length
236+
end
237+
238+
# Площадь прямоугольника равна произведению его длины и ширины
239+
def rectangle_area(height, width) do
240+
height * width
241+
end
242+
end
243+
```
244+
245+
Теперь автодополнение будет нам помогать. Мы сразу видим, площади каких фигур считает модуль и какие аргументы принимает функция.
246+
247+
![](/elixir_patternmatching:good_autocomplete.png)
248+
249+
# Итог:
250+
251+
Если подытожить, то паттернматчинг с перегрузкой функций допустим, когда:
252+
1. Функция принимает разное количество аргументов и контекст понятен без чтения исходного кода;
253+
254+
2. Тип передаваемых аргументов отличается, как в примере фукнции сортировки в начале статьи;
255+
256+
3. Необходима реализация рекурсии.

0 commit comments

Comments
 (0)