Перечисления и потоки

Перечисления

Эликсир позволяет работать с перечислениями с помощью модуля Enum. Мы уже познакомились с двумя перечисляемыми типами – списками и словарями.

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]

iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

Модуль Enum предоставляет огромное множество функция для трансформации, сортировки, группировки, фильтрации и извлечения элементов из перечислений. Это один из наболее часто используемых эликсирщиками модулей:

В Эликсире также есть диапазоны:

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]

iex> Enum.reduce(1..3, 0, &+/2)
6

Функции в модуле Enum ограничиваются, как понятно из названия, перечислением значений в структурах данных. Для специфических операций, вроде добавления или обновления элементов, возможно понадобится модуль соответствующей структуры данных. Например, если вы хотите добавить элемент на некоторую позицию в списке, вам нужна функция List.insert_at/3 из модуля List, потому что было бы мало смысла во вставке значения, например, в диапазон.

Можно сказать, что функции в модуле Enum полиморфны, ведь они могут работать с разными типами данных. В частности, функции из модуля Enum могут работать с любым типом данных, который реализует протокол Enumerable. Мы поговорим о протоколах позднее, а сейчас же перейдём к особому виду перечислений, называемых потоками.

Жадная или ленивая работа

Все функции из модуля Enum жадные. Многие функции, ожидающие на вход перечисление, возвращают списки:

iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>

iex> Enum.filter(1..3, odd?)
[1, 3]

Это значит, что при выполнении нескольких операций с Enum, каждая из них создаст промежуточный список, пока не будет достигнут результат:

iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

В пример выше есть последовательность операций. На вход берётся диапазон, каждый элемент которого умножается на 3. Первая операция создаст и вернёт список со 100_000 элементов. Затем мы выбираем все нечётные элементы из этого списка, что создаст новый список с 50_000 элементов. Затем суммируем все значения.

Пайп-оператор

Символ |> в коде выше называется пайп-оператором. Он принимает вывод из выражения слева и передаёт его первым аргументом в вызов функции справа. Он аналогичен оператору | в Юниксе. Его задача – явно выделить данные, которые будут преобразованы несколькими функциями. Чтобы увидеть, как это сделает код чище, взгляните на пример выше, переписанный без оператора |>:

iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000

Больше информации о пайп-операторе можно найти в его документации.

Потоки

В качестве альтернативы Enum, в Эликсире есть модуль Stream, который поддерживает ленивые операции:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

В отличии от перечислений, потоки – ленивы.

В примере выше выражение 1..100_000 |> Stream.map(&(&1 * 3)) возвращает тип данных, являющийся на самом деле потоком, который представляет собой вычисление функции map на диапазоне 1..100_000:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

Более того, они комбинируются, ведь мы можем собрать в пайплайн много потоковых операций:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

Вместо генерации промежуточных списков, потоки выстраивают серию вычислений, которая будет выполнена только когда мы передадим поток в модуль Enum. Потоки полезны при работе с большими, возможно бесконечными, коллекциями.

Многие функции в модуле Stream принимают любое перечисление в качестве аргумента и возвращают поток в качестве результата. Также там есть функции для создания потоков. Например, функция Stream.cycle/1 может быть использована для создания потока, который зацикливает переданное перечисление бесконечно. Будьте осторожны с тем, чтобы не вызвать функцию в духе функции Enum.map/2 на таком потоке, она зациклится навсегда:

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.cycle/1>

iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

С другой стороны, функция Stream.unfold/2 может быть использована для генерации значений из одного переданного значения:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>

iex> Enum.take(stream, 3)
["h", "e", "ł"]

Другая интересная функция – Stream.resource/3, которая может быть использована для оборачивания ресурсов, с гарантией, что они будут корректно открыты перед последовательным проходом, и закрыты после, даже в случае неудачи. Например, мы можем использовать её для потокового чтения файла:

iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>

iex> Enum.take(stream, 10)

Пример выше получит первые 10 строк выбранного файла. Это значит, что потоки могут быть очень полезны для работы с большими файлами или даже с медленными ресурсами, например, сетевыми.

Объём функциональности модулей Enum и Stream может показаться пугающим, но вы будете знакомиться с ними постепенно. В частности, сосредоточьтесь на модуле Enum для начала и переходите к Stream только в случае, когда появится необходимость в ленивой работе. Или для работы с медленными ресурсами, или с большими, возможно бесконечными, коллекциями.

Далее мы взглянем на основу Эликсира – процессы, которые позволяют писать конкурентные, параллельные и распределённые программы простым и понятным способом.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.