|
| 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 | + |
| 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 | + |
| 248 | + |
| 249 | +# Итог: |
| 250 | + |
| 251 | +Если подытожить, то паттернматчинг с перегрузкой функций допустим, когда: |
| 252 | +1. Функция принимает разное количество аргументов и контекст понятен без чтения исходного кода; |
| 253 | + |
| 254 | +2. Тип передаваемых аргументов отличается, как в примере фукнции сортировки в начале статьи; |
| 255 | + |
| 256 | +3. Необходима реализация рекурсии. |
0 commit comments