Обработчик списка

Обработчик списка — удобный инструмент для перепостроения списка (списков) с последующей обработкой. То же: list comprehension.

Если нам надо с каждым элементом списка проделать какую-то операцию, то часто используем библиотечную функцию lists:map/2.

1> lists:map(fun(X) -> X*X end, [1,2,3,4]).
[1,4,9,16]

Определение этой функции выглядит так:

map(Fun, List1) -> List2

Первый аргумент — фунтерм, с помощью которого мы будем обрабатывать каждый элемент списка. Второй аргумент — сам этот список. А в итоге возвращается List2 — ещё один список, состоящий из переработанных значений. В нашем пример возвращается [1,4,9,16].

Обработчик списка способен на такое же, но:

Та же задачка:

1> [ X*X || X <- [1,2,3,4] ].
[1,4,9,16]

В Эрланге часто принято давать переменным короткие имена, состоящие из одного-двух символов. Это может заметно повысить читабельность кода. Для обработчиков списков это особенно актуально:

1> L = [1,2,3,4].
[1,2,3,4]
2> [ X*X || X <- L ].
[1,4,9,16]

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

В общем виде обработчик списка выглядит так:

[ Выражение || Квалификатор1, Квалификатор2... ]

В примере выше был только один квалификатор (генератор X <- L). Квалификаторы бывают двух видов:

Что такое генераторы

Генераторы выглядят так:

Паттерн <- Выражение

Выражение должно вычисляться в список. Это может быть какой-нибудь литерал вроде [1,2,3,4,5]. Может быть, например, вызов какой-нибудь функции вроде lists:seq(1,5).

Паттерн может быть любым. Если ни одно значение из генерируемого списка не подошло, весь обработчик списка вернёт просто пустой список [].

То есть есть список L, который по одному отдаёт значения. Значения сопоставляются с паттерном. В большинстве случае паттерн содержит одну или нескольких переменных, к которым привязываются значения. Переменную X можно в дальнейшем использовать или в следующих квалификаторах (например, фильтрах), или в выражении слева от ||.

Хотя обычно в обработчике списка используются переменные, можно обойтись и без них.

1> [ ok || 777 <- lists:seq(1,1000) ].
[ok]
2> [ ok || 7777 <- lists:seq(1,1000) ].
[]

В этом примере мы получаем [ok], если наше число содержится в каком-то списке, а если нет — [].

Несколько генераторов подряд

Предположим, у нас есть список футбольных клубов. Его мы сделали в силу каких-то причин состоящим из атомов. Но дальше нам надо преобразовать атомы в строки. Для этого мы используем atom_to_list/1.

1> Fk = [spartak, zenit, dinamo, cska, torpedo, loko].
[spartak,zenit,dinamo,cska,torpedo,loko]
2> [ atom_to_list(X) || X <- Fk ].
["spartak","zenit","dinamo","cska","torpedo","loko"]

После || можно поставить не один, а два, три и т.д. генератора. X <- L1, Y <- L2 даёт нам уникальную возможность найти все уникальные пары XY. Если бы L1 был днями недели, а L2 — числами от 1 до 31, то генератор X <- L1, Y <- L2 породил бы все возможные сочетания дней недели и чисел: “Понедельник 1”, “Пятница 13” и т.д.

Попробуем составить турнирную таблицу из наших шести команд на сезон.

1> [ {X, Y} || X <- Fk, Y <-Fk ].         
[{spartak,spartak},
 {spartak,zenit},
 {spartak,dinamo},
 {spartak,cska},
 {spartak,torpedo}, ...

Список возможных пар составляет 6 * 6 = 36 единиц. Мы его полностью не приводим. В целом неплохо, но есть, однако, проблема: клуб spartak не может встретиться с клубом spartak. И другие клубы тоже не могут встретиться сами с собой. В футболе нужен соперник.

Что такое фильтры

Помимо генераторов, как уже говорилось, в обработчике списка можно использовать фильтры. Фильтры нужны для фильтрации, то есть чтобы одни значения выпустить из генератора, а другие — нет. Фильтры в обработчике списка могут быть:

С помощью фильтра мы можем быстро получить желаемую турнирную таблицу.

[ {X, Y} || X <- Fk, Y <- Fk, X =/= Y ].
[{spartak,zenit},
 {spartak,dinamo},
 {spartak,cska},
 {spartak,torpedo},
 {spartak,loko},
 {zenit,spartak},
 {zenit,dinamo}, ...

Вот теперь всё нормально. Сдвоенный генератор “X <- Fk, Y <- Fk” даёт нам всевозможные пары, а фильтр “X =/= Y” следит за тем, чтобы X и Y были разными командами (“=/=” применяется для аккуратного сравнения термов на неравенство).

Чем хорош наш пример, так это тем, что хорошо видно, как работает цепочка квалификаторов. Сначала срабатывает первый генератор (X <- Fk), он отдаёт первое значение (spartak), затем срабатывает второй генератор (Y <- Fk), он отдаёт своё первое значение (тоже spartak), затем срабатывает фильтр — в данном случае он производит false, поэтому данная пара X и Y отбрасывается.

Что происходит дальше? Квалификаторы выполняются слева направо. Поэтому логично, что именно первый квалификатор (X <- Fk) пока замерзает и не отдаёт нового значения. Следующая итерация, таким образом, начинается со второго квалификатора-генератора (Y <- Fk). Тот отдаёт атом zenit. Пара spartak и zenit поступает на фильтр и проходит его. X и Y попадают в выражение слева от ||, и оттуда уже выходит окончательный элемент — кортеж {spartak,zenit}. Он будет первым элементом, который возвращает наш обработчик списка.

Теперь хочется поэкспериментировать с ещё более сложной последовательностью квалификаторов. Давайте предположим, что мы проводим другой турнир — только для московских команд. Поэтому команда zenit не будет принимать участия.

1> [ {X, Y} || X <- Fk, X =/= zenit, Y <-Fk, Y =/= zenit, X =/= Y ].
[{spartak,dinamo},
 {spartak,cska},
 {spartak,torpedo},
 {spartak,loko},
 {dinamo,spartak}, ...

Мы добавили два фильтра: после генератора “X <- Fk” идёт проверка, не zenit ли значение у X. Если zenit, то дальше уже ничего не срабатывает, потому что лишено смысла. Аналогичная проверка происходит после генератора “Y <- Fk”. Если фильтр возвращает false, оставшиеся квалификаторы уже не срабатывают.

В фильтрах можно использовать предикаты. В этом примере мы с помощью предиката is_integer/1 оставляем только целые числа:

1> L = [1, 2, 3.14, 4, 5, six, seven, {8}].
[1,2,3.14,4,5,six,seven,{8}]
2> [ X || X <- L, is_integer(X) ].
[1,2,4,5]

Использование фунтермов

В фунтермах заключена большая мощь. Надо уметь ими пользоваться и не забывать про них. Часто с их помощью в Эрланге можно писать очень элегантный, эффектный код.

В обработчике списка в выражении слева от || можно использовать, конечно, любые функции и фунтермы. Создадим и привяжем к переменной Hypo простенькую функцию, которая по двум значениям катетов возвращает гипотенузу. А потом используем её для нахождения всех вариантов гипотенузы, когда один катет имеет целое значение от 1 до 4, а другой катет — от 5 до 8.

1> Hypo = fun (K1, K2) -> math:sqrt(K1*K1+K2*K2) end. 
#Fun<erl_eval.41.3316493>
2> [ Hypo(X,Y) || X <- [1,2,3,4], Y <- [5,6,7,8] ]. 
[5.0990195135927845,6.082762530298219,7.0710678118654755,
 8.06225774829855,5.385164807134504,6.324555320336759,
 7.280109889280518,8.246211251235321,5.830951894845301,
 6.708203932499369,7.615773105863909,8.54400374531753,
 6.4031242374328485,7.211102550927978,8.06225774829855,
 8.94427190999916]

Список может включать любые термы Эрланга, в том числе и фунтермы. Давайте создадим четыре фунтерма. Один будет складывать числа, второй — отнимать, третий — перемножать, а четвёртый — делить.

1> F1 = fun(A, B) -> A + B end. 
#Fun<erl_eval.41.3316493>
2> F2 = fun(A, B) -> A - B end. 
#Fun<erl_eval.41.3316493>
3> F3 = fun(A, B) -> A * B end. 
#Fun<erl_eval.41.3316493>
4> F4 = fun(A, B) -> A / B end. 
#Fun<erl_eval.41.3316493>
5> [ Z(X, Y) || X <- [100, 200, 300], Y <- [1, 2, 3], Z <- [F1, F2, F3, F4] ].
[101,99,100,100.0,102,98,200,50.0,103,97,300,
 33.333333333333336,201,199,200,200.0,202,198,400,100.0,203,
 197,600,66.66666666666667,301,299,300,300.0,302|...]
6> 

Этот обработчик списка содержит три генератора. Первые два генерируют числа. Эти значения привязываются к переменным X и Y. Третий генератор возвращает один из четырёх фунтермов. Функциональный терм привязывается к переменной Z. И в выражении слева от || мы просто записываем Z(X,Y). Первые два генератора сначала породят пару чисел 100 и 1. Для них сначала отработает функция F1 (сложение), потом функция F2 (вычитание), затем функция F3 (умножение), ну и F4 (деление). Затем то же самое будет проделано по отношению к паре 100 и 2. И так далее.

Обработка запроса к ETS/DETS

Обработчик списка можно использовать, например, для изящного изъятия данных из таблиц ETS или DETS.

main() ->
    Ets = ets:new(?MODULE, [bag]),
    [ ets:insert(Ets, {number, X}) || X <- lists:seq(1, 1000) ],
    Result = [ X || {number, X} <- ets:lookup(Ets, number) ],
    io:format("Result: ~p~n", [Result]),
    ets:delete(Ets).

В этом примере мы создаём в оперативной памяти ETS-таблицу типа bag. Поэтому одному ключу может быть сопоставлено множество значений. Мы заполняем таблицу кортежами вида {number, X}, где Х — целое число от 1 до 1000.

Затем мы делаем запрос к ETS — просим выдать все кортежи, где ключ — атом number. Функция ets:lookup/2 отдаёт список из таких кортежей (в списке тысяча кортежей). Нам, может быть, совсем не нужны кортежи, нам нужен просто список из целых чисел. Поэтому мы и используем обработчик списка в таком вот виде.

Обработчик списка как фильтр

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

1> [X || {red,X} <- [{red,128},{grn,64},{blu,200},{red,254}]]. 
[128,254]

Когда очередной элемент проваливает сопоставление с паттерном, то ничего страшного не происходит (как это может случиться в функциях, где ни один паттерн не подойдёт). Просто из генератора берётся следующий элемент.

Комбинации нескольких обработчиков списка

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

1> L = "АНТАРКТИДА". 
"АНТАРКТИДА"
2> [Y||{_,Y} <- lists:sort([{rand:uniform(),X} || X <- L])].
"НААРАДИТТК"

Генератор X <- L отдаёт по одному элементу (букве). Каждая буква попадает в выражение {rand:uniform(),X}. Там из неё получается элемент — кортеж, в котором сначала идёт случайное вещественное число. Выходит что-то вроде {0.8133925319887593,1040}. Именно список таких кортежей и возвращает тот обработчик, который находится внутри другого обработчика.

Этот список кортежей в качестве аргумента поступает в функцию lists:sort/1. В Эрланге вообще можно отсортировать какие угодно термы (согласно принципам сортировки). В кортежах первым элементом является вещественное число, по ним и будет проходить сортировка. Именно так мы и получаем случайное перемешивание. Но осталось избавиться от кортежей и случайных чисел в них.

lists:sort является частью генератора внешнего обработчика списка. Кортежи по одному сопоставляются с паттерном {_,Y}. Случайные числа нам больше не нужны и поэтому уходят в небытие (анонимную переменную). Вуаля — теперь нужные нам элементы (буквы) привязываются к переменной Y. Слева от || всё выражение состоит только из Y, потому что нам больше уже не надо обрабатывать элементы. Элементы группируются в список — и мы его получаем.


© Алексей Карманов, 2024.