Исходный код Перечислимые и потоки
Хотя 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.17.2/enumerable-and-streams.html