Перечисления
Эликсир позволяет работать с перечислениями с помощью модуля 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
только в случае, когда появится необходимость в ленивой работе. Или для работы с медленными ресурсами, или с большими, возможно бесконечными, коллекциями.
Далее мы взглянем на основу Эликсира – процессы, которые позволяют писать конкурентные, параллельные и распределённые программы простым и понятным способом.