Списковые выражения

В Эликсире часто нужно пройти в цикле по Enumerable, для того чтобы произвести какие-то преобразования над данными и передать их дальше. Списковые выражения – особый синтаксический сахар для таких задач: они позволяют использовать данные со специальной формой for.

Например, чтобы получить из списка чисел список их квадратов, потребуется сделать следующее:

iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]

Списковые выражения состоят из трех частей: генераторы, фильтры и Collectable-структуры.

Генераторы и фильтры

В выражении выше часть n <- [1, 2, 3, 4] является генератором. Он в прямом смысле генерирует значения, которые будут использоваться в списковом выражении. С правой стороны выражения могут находиться любые перечисления:

iex> for n <- 1..4, do: n * n
[1, 4, 9, 16]

Генераторы также поддерживают операцию сопоставления с образцом в левой части выражения. Все неподходящие значения будут просто проигнорированы. Для примера представим, что у нас есть список с ключевыми словами, где ключами могут быть атомы :bad или :good, а мы хотим посчитать квадраты значений для ключей :good:

iex> values = [good: 1, good: 2, bad: 3, good: 4]

iex> for {:good, n} <- values, do: n * n
[1, 4, 16]

В качестве альтернативы соответствия шаблону можно использовать фильтры, чтобы выбрать какие-то конкретные значения. Скажем, нам нужны только числа, которые делятся на 3:

iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end

iex> for n <- 0..5, multiple_of_3?.(n), do: n * n
[0, 9]

Списковые выражения пропускают только те значения, для которых фильтр возвращает false или nil, остальные значения обрабатываются.

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

dirs = ['/home/mikey', '/home/james']
for dir  <- dirs,
    file <- File.ls!(dir),
    path = Path.join(dir, file),
    File.regular?(path) do
  File.stat!(path).size
end

Несколько генераторов могут использоваться для получения прямого произведения двух списков:

iex> for i <- [:a, :b, :c], j <- [1, 2], do:  {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]

Теперь рассмотрим получение пифагоровых троек в качестве более сложного примера. Пифагорова тройка – набор из трех натуральных чисел, таких что a*a + b*b = c*c. Давайте создадим файл triple.exs:

defmodule Triple do
  def pythagorean(n) when n > 0 do
    for a <- 1..n,
        b <- 1..n,
        c <- 1..n,
        a + b + c <= n,
        a*a + b*b == c*c,
        do: {a, b, c}
  end
end

Теперь выполним в терминале:

iex triple.exs
iex> Triple.pythagorean(5)
[]

iex> Triple.pythagorean(12)
[{3, 4, 5}, {4, 3, 5}]

iex> Triple.pythagorean(48)
[{3, 4, 5}, {4, 3, 5}, {5, 12, 13}, {6, 8, 10}, {8, 6, 10}, {8, 15, 17},
 {9, 12, 15}, {12, 5, 13}, {12, 9, 15}, {12, 16, 20}, {15, 8, 17}, {16, 12, 20}]

Для больших чисел операции выше становятся очень дорогими, более того, наш код выдает дубликаты: {a, b, c}, представляющие собой одинаковую тройку с {b, a, c}. Но текущее списковое выражение может быть значительно оптимизировано таким образом, чтобы переменные из прошлого генератора использовались в следующем. Заодно и от дубликатов избавимся:

defmodule Triple do
  def pythagorean(n) when n > 0 do
    for a <- 1..n-2,
        b <- a+1..n-1,
        c <- b+1..n,
        a + b + c <= n,
        a*a + b*b == c*c,
        do: {a, b, c}

    IO.inspect({a, b, c})
  end
end

Необходимо помнить, что все переменные, созданные внутри спискового выражения (внутри генераторов, фильтров или переменных внутри самого фрагмента кода) недоступны извне.

Генераторы битовых строк

Генераторы битовых строк также поддерживаются. Они очень полезны, когда вам необходимо пройти по потокам битовых строк. Следующий пример получает на вход список пикселей из двоичных данных и создает из него кортежи по три значения: красный, зеленый и синий соответственно:

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>

iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

Генераторы битовых строк могут быть использованы вместе с обычными генераторами и фильтрами.

Опция :into

В примерах выше все списковые выражения возвращали список в качестве результата. Однако, результат может быть преобразован к другому типу при помощи опции :into.

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

iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

Множества, словари и другие типы могут быть переданы в опцию :into, главное, чтобы они реализовывали протокол Collectable.

Частый случай использования опции :into – модифицирование значений словаря без изменения ключей:

iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}

Еще один пример с использованием потоков. Так как встроенный модуль IO предоставляет нам возможность работать с потоками (а потоки реализуют оба протокола: Enumerable и Collectable), то с помощью списковых выражений можно очень легко реализовать следующую логику: пользователь вводит строку, она приводится к верхнему регистру и выводится обратно:

iex> stream = IO.stream(:stdio, :line)
iex> for line <- stream, into: stream do
...>   String.upcase(line) <> "\n"
...> end

Теперь можно ввести в консоль что угодно и увидеть, что то же самое, но в верхнем регистре, будет выведено обратно. Но, к сожалению, теперь IEx застрял в списковом выражении. Чтобы выйти, нужно будет нажать Ctrl+C дважды.

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