Source Перечислимые и потоки
Хотя Elixir позволяет нам писать рекурсивный код, большинство операций, выполняемых над коллекциями, выполняется с помощью модулей Enum и Stream. Давайте узнаем как.
Перечислимые
Elixir предоставляет концепцию перечислимых и модуль 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 предоставляет широкий спектр функций для преобразования, сортировки, группировки, фильтрации и извлечения элементов из перечислимых объектов. Это один из часто используемых модулей разработчиками в Elixir. Для общего обзора всех функций в модуле Enum см. справочник по Enum.
Elixir также предоставляет диапазоны (см. Range), которые также являются перечислимыми:
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, поскольку вставка значения, например, в диапазон, не имеет смысла.
Мы говорим, что функции в модуле Enum полиморфны, потому что они могут работать с различными типами данных. В частности, функции в модуле Enum могут работать с любым типом данных, который реализует протокол Enumerable. Мы обсудим протоколы в последующей главе, а сейчас перейдём к определённому виду перечислимого объекта — потоку.
Жадный против ленивого
Все функции в модуле Enum жадные. Многие функции ожидают перечислимый объект и возвращают список:
iex> odd? = fn x -> rem(x, 2) != 0 end #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, Elixir предоставляет модуль 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.unfold/2> 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, которая может быть использована для обертывания ресурсов, гарантируя, что они открыты непосредственно перед перечислением и закрываются после него, даже в случае ошибок. Например, File.stream!/1 строится на основе Stream.resource/3 для потоковой передачи файлов:
iex> stream = File.stream!("path/to/file")
%File.Stream{
line_or_bytes: :line,
modes: [:raw, :read_ahead, :binary],
path: "path/to/file",
raw: true
}
iex> Enum.take(stream, 10)
В примере выше будут извлечены первые 10 строк выбранного файла. Это означает, что потоки могут быть очень полезны для обработки больших файлов или даже медленных ресурсов, таких как сетевые ресурсы.
Модули Enum и Stream предоставляют широкий спектр функций, но вам не нужно запоминать все их. Ознакомьтесь с Enum.map/2, Enum.reduce/3 и другими функциями с map или reduce в их названиях, и вы естественным образом сформируете интуицию по наиболее важным случаям использования. Вы также можете сначала сосредоточиться на модуле Enum и перейти к модулю Stream только в тех конкретных сценариях, где требуется ленивость, чтобы либо обрабатывать медленные ресурсы, либо большие, возможно бесконечные, коллекции.
Далее мы рассмотрим центральную для Elixir функцию — процессы, которые позволяют нам писать конкурентные, параллельные и распределённые программы простым и понятным способом.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/enumerable-and-streams.html