Перейти до основного вмісту

Рекурсія в Elixir

Рекурсія — це коли функція викликає саму себе. В Elixir, де немає циклів, рекурсія є основним способом обробки колекцій та повторюваних операцій.

Проста рекурсія

Базова рекурсивна функція складається з двох частин: базового випадку та рекурсивного виклику:

# Факторіал
defmodule Math do
  def factorial(0), do: 1
  def factorial(n) when n > 0 do
    n * factorial(n - 1)
  end
end

Math.factorial(5)
# 120 (5 * 4 * 3 * 2 * 1)

# Сума списку
defmodule ListHelper do
  def sum([]), do: 0
  def sum([head | tail]) do
    head + sum(tail)
  end
end

ListHelper.sum([1, 2, 3, 4, 5])
# 15
Примітка: Базовий випадок (наприклад, порожній список або 0) запобігає нескінченній рекурсії.

Проблема простої рекурсії

Проста рекурсія створює стек викликів, що може призвести до переповнення пам'яті:

# Неефективна рекурсія
def factorial(5)
  5 * factorial(4)
    4 * factorial(3)
      3 * factorial(2)
        2 * factorial(1)
          1 * factorial(0)
            1
          # Тепер обчислюємо назад:
          1 * 1 = 1
        2 * 1 = 2
      3 * 2 = 6
    4 * 6 = 24
  5 * 24 = 120

# Для великих чисел це займає багато пам'яті!

Хвостова рекурсія

Хвостова рекурсія — це коли рекурсивний виклик є останньою операцією функції. Elixir оптимізує такі виклики:

defmodule Math do
  # Хвостова рекурсія з акумулятором
  def factorial(n), do: factorial(n, 1)
  
  defp factorial(0, acc), do: acc
  defp factorial(n, acc) when n > 0 do
    factorial(n - 1, n * acc)
  end
end

Math.factorial(5)
# 120

# Хвостова рекурсія для суми списку
defmodule ListHelper do
  def sum(list), do: sum(list, 0)
  
  defp sum([], acc), do: acc
  defp sum([head | tail], acc) do
    sum(tail, acc + head)
  end
end
Порада: У хвостовій рекурсії використовується акумулятор (acc) для збереження проміжного результату. Це дозволяє оптимізувати виклики.

Порівняння: проста vs хвостова рекурсія

Характеристика Проста рекурсія Хвостова рекурсія
Пам'ять O(n) — стек викликів O(1) — константна
Швидкість Повільніше Швидше
Читабельність Часто простіша Потребує акумулятор
Використання Малі дані Великі дані

Практичні приклади

# Довжина списку
defmodule ListOps do
  def length(list), do: length(list, 0)
  
  defp length([], acc), do: acc
  defp length([_head | tail], acc) do
    length(tail, acc + 1)
  end
end

# Реверс списку
defmodule ListOps do
  def reverse(list), do: reverse(list, [])
  
  defp reverse([], acc), do: acc
  defp reverse([head | tail], acc) do
    reverse(tail, [head | acc])
  end
end

ListOps.reverse([1, 2, 3])
# [3, 2, 1]

# Map для списку
defmodule ListOps do
  def map(list, func), do: map(list, func, [])
  
  defp map([], _func, acc), do: reverse(acc)
  defp map([head | tail], func, acc) do
    map(tail, func, [func.(head) | acc])
  end
  
  defp reverse(list), do: reverse(list, [])
  defp reverse([], acc), do: acc
  defp reverse([h | t], acc), do: reverse(t, [h | acc])
end

ListOps.map([1, 2, 3], &(&1 * 2))
# [2, 4, 6]

Коли використовувати рекурсію

  • Обробка списків: коли потрібно пройтися по всіх елементах
  • Деревоподібні структури: обхід дерев, вкладених даних
  • Математичні обчислення: факторіал, Фібоначчі, GCD
  • Розділяй і володарюй: швидке сортування, злиття
Увага: Для простих операцій зі списками краще використовувати модуль Enum замість власної рекурсії. Пишіть рекурсивні функції тільки коли це дійсно потрібно.

Альтернатива: модуль Enum

# Замість рекурсії використовуйте Enum
list = [1, 2, 3, 4, 5]

# Замість власної sum
Enum.sum(list)
# 15

# Замість власної map
Enum.map(list, &(&1 * 2))
# [2, 4, 6, 8, 10]

# Замість власної filter
Enum.filter(list, &(&1 > 3))
# [4, 5]

# Складні операції
list
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 3))
|> Enum.reduce(0, &+/2)
# 18

Ключові поняття

  • Базовий випадок: умова виходу з рекурсії (обов'язкова!)
  • Рекурсивний випадок: виклик функції з меншими даними
  • Акумулятор: змінна для збереження проміжних результатів
  • Хвостовий виклик: рекурсивний виклик як остання операція
  • TCO: Tail Call Optimization — оптимізація хвостових викликів
Золоте правило: Якщо функція може зробити щось після рекурсивного виклику (наприклад, множення), це не хвостова рекурсія. Якщо рекурсивний виклик — це останнє, що робить функція, це хвостова рекурсія.

Коментарі

Популярні публікації

Колекції в Elixir

Списки Що таке список у Elixir У Elixir список — це впорядкована колекція елементів, реалізована як однозв’язний список . Це означає, що кожен елемент (вузол) зберігає посилання на наступний, але не на попередній. Така структура дозволяє швидко додавати елементи на початок списку, але повільно доступати до довільного елементу. У Java подібну структуру представляє LinkedList — частина Java Collections Framework. Вона реалізована як двозв’язний список, що забезпечує зручне додавання/видалення елементів з початку або кінця списку. Створення списків У Elixir список створюється за допомогою квадратних дужок: list = [1, 2, 3, 4] Додавання елементів У Elixir новий елемент можна додати тільки на початок списку за допомогою оператора | : # Elixir list = [2, 3, 4] new_list = [1 | list] # [1, 2, 3, 4] Доступ до елементів У Elixir немає прямого доступу до елементів за індексом, але це можна зробити через Enum.at : Enum.at([10, 20, 30], 1) # 20 Ітерація по списку У Elixir ...

Інструменти для роботи з Node.js

Що таке npm? npm (Node Package Manager) — це офіційний пакетний менеджер для Node.js . Він дозволяє: Встановлювати сторонні бібліотеки та фреймворки Керувати залежностями проєкту Запускати скрипти (команди) через package.json Приклад ініціалізації проєкту з npm npm init -y Файл package.json (скорочений приклад) { "name": "my-project", "version": "1.0.0", "scripts": { "start": "ts-node src/index.ts", "build": "tsc" }, "dependencies": { "express": "^4.18.0" }, "devDependencies": { "typescript": "^5.0.0", "ts-node": "^10.0.0" } } Що таке tsconfig.json? tsconfig.json — це файл конфігурації для компілятора TypeScript, який визначає, як слід компілювати код. Приклад файлу tsconfig.json { "compilerOptions": { "target": "ES2020...

Атоми в мові програмування Elixir

Атоми в Elixir Атоми є фундаментальною концепцією в Elixir , що відіграє ключову роль у створенні надійних та масштабованих систем. В Elixir це специфічний тип даних, який є константою , незмінною , ідентифікованою за своїм ім'ям . Отже, атом в Elixir — це іменована константа, що представляє себе. Уявіть, що ви даєте унікальне ім'я певній речі, і це ім'я завжди посилається саме на цю річ, і ніколи на щось інше. Наприклад, атом :ok завжди буде означати саме успішне завершення операції, а не якесь інше значення. Технічно, атоми є похідними від чисел . Кожен унікальний атом зберігається у таблиці атомів, і йому присвоюється унікальний цілочисельний ідентифікатор. Це робить їх надзвичайно ефективними для порівняння: замість порівняння рядків (що є повільною операцією), Elixir порівнює цілочисельні ідентифікатори. Переваги та особливості використання атомів Переваги атомів: Ефективність. Завдяки своєму числовому представленню, порівняння атомів є дуже швидким. Це осо...

Основи Node.js

Що таке Node.js? Node.js — це середовище виконання JavaScript поза браузером, побудоване на рушії Google V8 . Воно дозволяє запускати JavaScript на сервері, створюючи серверні застосунки з високою продуктивністю. Основні характеристики: Однопотокова модель з неблокуючим I/O Асинхронне виконання за допомогою event loop Висока продуктивність у роботі з мережевими запитами npm — найбільший реєстр пакетів Що таке event loop? Event loop — це механізм в Node.js, який дозволяє неблокуючим асинхронним операціям виконуватись у середовищі з єдиним потоком. Він постійно перевіряє наявність подій у черзі та викликає відповідні колбеки. Як працює однопоточність у Node.js? Node.js використовує один потік (main thread) для обробки JavaScript-коду. Операції, які займають час (мережеві запити, читання з файлової системи), делегуються до системних API або thread pool, і після завершення результат повертається у основний потік через event loop. Приклад: асинхронна о...

Встановлення PostgreSQL на Ubuntu-сервер

Встановлення Оновлюємо пакети та встановлюємо PostgreSQL: sudo apt update sudo apt install -y postgresql postgresql-contrib Перевіряємо статус сервісу: sudo systemctl status postgresql Якщо PostgreSQL не запущений, запустимо його: sudo systemctl start postgresql sudo systemctl enable postgresql Налаштування безпеки Зміна пароля: sudo -u postgres psql У консолі PostgreSQL: ALTER USER postgres PASSWORD 'міцний_пароль'; \q \q - вихід з консолі. Список основних команд для роботи з PostgreSQL можна переглянути за посиланням. За замовчуванням PostgreSQL слухає localhost (127.0.0.1). Щоб дозволити доступ із зовнішніх машин, редагуємо конфігурацію: sudo nano /etc/postgresql/17/main/postgresql.conf (замість 17 вкажи версію PostgreSQL, яку встановлено) Шукаємо рядок: #listen_addresses = 'localhost' та замінюємо на listen_addresses = '*' Зберігаємо (Ctrl + X, Y, Enter). Тепер редагуємо pg_hba.conf: sudo nano /etc/postgresql/17/main/pg_hba.conf...