Программирование на Athanor – 11 (дополнительные списковые операции)
Операции над списками с функциональными операндами
В предыдущей статье были подробно рассмотрены строковые операции, имеющие операнды – функциональные ссылки. Немало таких операций определено и для операндов-списков; многие из них и по названиям, и по функциональности напоминают свои строковые аналоги. Однако, есть и существенные различия. Во-первых, списки устроены существенно сложнее строк: они гетерогенные, то есть могут иметь элементы разного типа (включая и вложенные элементы-списки). Во-вторых, строки неизменяемы, а списки – мутабельны, т.е. их можно изменять. Что и определяет многие различия между строковыми и списковыми операциями.
Однако (прежде, чем подробно рассматривать эти операции), упомянем пару операций, которые позволяют довольно просто вставлять/удалять элементы списка:
|
List [<-] FromValues
|
l_push (List, FromValues)
|
Вставить (в начало списка List) значения из списка FromValues (в обратном порядке) |
|
List [->] ToMutables
|
l_pop (List, ToMutables)
|
Удалить (из начала списка List) значения в (мутабельный) список ToMutables |
Прежде всего заметим, что эти операции работают с началом списка List (в отличие от Perl или JavaScript, где функции push/pop выполняют изменения в конце списка). Другими словами, эти операции работают со списком List как со стеком, самая вершина которого находится в его начале. (Если помните: унарные и бинарные скалярные операции рассматривают списки примерно так же.)
Операция [<-] (она же l_push) осуществляет вставку элементов в начало списка List, извлекая их значения последовательно из списка FromList. (Таким образом, в List они заносятся в порядке, обратном их порядку в FromList!) Например:
- List = [1 2 3 4];
- List [<-] [5 6 7 8];
В результате список List получит значение (8, 7, 6, 5, 1, 2, 3, 4). Любой элемент из списка добавляемых значений сам может быть списком (но если последний элемент FromValues является списком – FromValues должен быть открытым). В частности, если нужно вставить список (например, ['aa' 'bb' 'cc']) как один элемент, это делается примерно так:
- List [<-] (['aa' 'bb' 'cc'], );
Обратная операция [->] (она же l_pop) извлекает элементы из начала списка List, и записывает их в список ToMutables, элементы которого должны быть мутабельными выражениями. (Чаще всего это, конечно, переменные.) Например:
- List [->] [a b c d];
реклама
В результате, список List вернется к (исходному) значению (1, 2, 3, 4), а другие переменные примут такие значения: a == 8, b == 7, c == 6, d == 5. Ни одно значение из оригинального списка не пропадет!
Обе эти операции (как и многие другие), рассматривают скалярные значения и () – как «вырожденные» варианты списков длины 1 и 0. Например, если исходное значение List пусто, т.е. (), вставка в него произвольного значения просто эквивалентна присваиванию этого значения List. Если List уже содержит скалярное значение Tail, после выполнения List [<-] Head значением List станет (Head, Tail). Разумеется, справедливо и обратное: если из двухэлементного списка извлечь один элемент, в списке останется последний элемент (если он скалярный – список превратится в скаляр). Если же из скалярной переменной "извлечь" ее значение, она станет равна (). (Понятно, что из () извлечь уже ничего нельзя.)
Сочетание операций l_push/l_pop с l_tail_by позволяет вставлять или удалять элементы в произвольном месте списка. Например, если L = (1, 2, 3, 4, 5), то после выполнения:
- l_tail_by (3, L) [<-] "+++";
– список L станет равен (1, 2, 3, "+++", 4, 5). Если после этого выполнить:
- l_tail_by (2, L) [->] [x y z];
– то список L станет равен (1, 2, 5); а переменные: x = 3, y = "+++", z = 4. В общем, идея должна быть понятна.
Через l_tail_by можно вставить новые элементы перед любым существующим элементом закрытого списка. Если список открытый – новые значения можно вставить и в самый его конец (это ещё одно преимущество открытых списков). Рассмотрим теперь ещё одну операцию, которая позволяет "разрезать" список на части.
|
l_split (n, List)
|
Разрезать список List на две части, в позиции n.
|
Функтор l_split «разрезает» список List таким образом, что его «головной» фрагмент содержит (внимание!) n+1 элемент, а «хвостовой» – все остальные. Разумеется, эта операция является мутатором для списка List. Впрочем, если n == 0, то ничего не происходит, и список вообще не изменяется (понятно, почему?)
Например, когда:
- ll = ['a' 'b' 'c' 'd' 'e' 'f'];
реклама
– после выполнения l_split(3, ll) – значение списка ll станет равным (('a', 'b', 'c', 'd'), 'e', 'f'). Другоми словами, l_head(ll) станет равно ('a', 'b', 'c', 'd'), а l_tail(ll) – ('e', 'f'). Заметим, что эта операция (в некотором роде) является обратной для l_cat (списковой конкатенации) – вызов:
- l_cat^l_split (n, List);
– просто возвращает "прежнее" значение списка List для любого n. (Понятно, почему?)
Заметим, что вот так легко можно определить: является ли список-операнд открытым или закрытым?
- ! is_open_list (L) = is_list^l_tail_by (#L - 1, L);
Вызов is_open_list возвращает 1, если его операнд-список является открытым (и 0, в противном случае).
А теперь перейдём к списковым функторам, имеющим своими операндами функциональные ссылки. Начнём с принимающих ссылки на предикаты. Как и в случае со строками, введём немного полезных терминов: будем называть элемент списка Pred-негативным (если применение к нему предиката Pred возвращает 0) или Pred-позитивным (в противном случае).
|
l_while (Pred, List)
|
Количество Pred-позитивных элементов в начале списка List
|
|
l_until (Pred, List)
|
Количество Pred-негативных элементов в начале списка List
|
Вот альтернативная формулировка: возвращается индекс для самого первого Pred-негативного (l_while) или Pred-позитивного (l_until) элемента. В граничных случаях: l_while возвращает 0 (если Pred-негативен самый первый элемент List) или #List (если ВСЕ элементы списка Pred-позитивны). Для l_until "негативность" и "позитивность" меняются местами. В общем, это аналоги строковых операций s_span_in и s_span_ex (только для списков).
|
l_while_r (Pred, List)
|
Разность между длиной списка List и количеством Pred-позитивных элементов в его конце
|
|
l_until_r (Pred, List)
|
Разность между длиной списка List и количеством Pred-негативных элементов в его конце
|
Аналогично l_while и l_until, только действуют не с начала списка List, а с его конца. В граничных случаях: l_while_r возвращает #List (если Pred-негативен самый последний элемент List) или 0 (если ВСЕ элементы списка Pred-позитивны). Для l_until_r "негативность" и "позитивность" меняются местами. (По своей функции, это "списковые" аналоги строковых операций s_rspan_in и s_rspan_ex.)
Вот несколько примеров:
- l_while (!(V) = (V <= 3), List)
Возвращает индекс первого элемента в List, строго большего 3 (предполагая, что все элементы списка – числа).
- l_until (!(V) = (V <= 3), List)
Наоборот: индекс первого элемента в List, меньшего или равного 3.
- l_while_r (!(V) = (V > 5), List)
Индекс первого с конца элемента в List, строго большего 5.
- l_until_r (!(V) = (V > 5), List)
Наоборот: индекс первого с конца элемента в List, меньшего или равного 5.
Рассмотренные ниже функторы – уже действуют на весь список, и позволяют выбрать из него элементы по некому критерию (заданному предикатом):
|
l_filter_in (Pred, List)
|
Возвращает список из всех Pred-позитивных элементов в List
|
|
l_filter_ex (Pred, List)
|
Возвращает список из всех Pred-негативных элементов в List
|
Вот примеры:
- l_filter_in (!(V) = (abs (V) < 10), List)
Результат – список из всех элементов List (положительных или отрицательных), абсолютная величина которых строго меньше 10.
- l_filter_ex (!is_string, List)
реклама
Результат – список из всех элементов List, которые не являются строками.
Следующие операции подсчитывают элементы в списке по заданному критерию:
|
l_count_in (Pred, List)
|
Подсчёт всех Pred-позитивных элементов List
|
|
l_count_ex (Pred, List)
|
Подсчёт всех Pred-негативных элементов List
|
Эти четыре функтора связаны очевидными тождествами:
- # l_filter_in (Pred, List) == l_count_in (Pred, List)
- # l_filter_ex (Pred, List) == l_count_ex (Pred, List)
Рассмотрим теперь операции, позволяющие конструировать списки. Например, l_map реализует отображение одного списка на другой:
|
l_map (Func, List)
|
Список из результатов применения функтора Func ко всем элементам списка List
|
Операция применяет функциональную ссылку Func (это уже не обязательно предикат!) к элементам списка List, возвращая список из полученных результатов. Вот несколько примеров:
- l_map (! (x) = (x * 3), List)
Это возвращает результат поэлементного умножения списка List на 3 (предполагая, что все его элементы – числовые, или к ним приводятся). Например, если List = (10, 25, 30) – то результатом будет список (30, 75, 90).
- l_map (! (s) = (#$ s), List)
Возвращает список из строковых длин элементов списка List (предполагая, что они все строки, или к ним приводятся). Например, если List = ('alpha', 'beta', 'gamma') – то результатом выполнения будет список (5, 4, 5).
- l_map (! (str) = (s_filter_ex (str, ! cc_blank)), List)
Возвращает список элементов (также молча предполагая, что все они строковые!) списка List с "вычищенными" из них пробелами (и всеми прочими пробельными символами, позитивными для предиката cc_blank).
Результат l_map всегда имеет ту же длину, что и список-операнд List. Более того, l_map сохраняет такой атрибут списка как его "открытость": если список List открытый – то и результат тоже будет открытым списком. По обычной логике, если операнд List окажется скаляром, то возвращается просто значение (Func ! List), а если операнд равен () – то и значением будет ().
Следующие две операции трансформируют произвольный диапазон значений в список:
|
l_range (From..To, Func)
|
Список из результатов применения функтора Func ко всем целым (по возрастанию) в диапазоне From..To
|
|
l_range_r (From..To, Func)
|
Список из результатов применения функтора Func ко всем целым (по убыванию) в диапазоне From..To
|
Понятие "диапазона" мы уже рассматривали (и не раз), но повторим: From..To задаёт все целые числа от (включая!) нижней границы From до (не включая!) верхней границы To. Функтор l_range превращает этот диапазон в возрастающий список содержащихся в нём значений (от From до To-1); l_range_r делает то же самое, но по убыванию (от To-1 до From). Если задан функциональный аргумент Func – то он применяется к каждому из значений, и возвращается уже список из полученных результатов. Если же Func не задан – это не ошибка, и возвращается просто сам список целых чисел в диапазоне.
Заметим, что в этом случае запятая в списке не должна быть опущена! (Без неё, список From..To будет восприниматься не как один аргумент, а как два, что некорректно!) Вот примеры:
- l_range (15..20, )
Результат: список (15, 16, 17, 18, 19).
- l_range_r (15..20, )
Результат: список (19, 18, 17, 16, 15).
- l_range (15..20, !(n) = (2*n - 1))
Результат: список (29, 31, 33, 35, 37).
- l_range_r (15..20, !(n) = (2*n - 1))
Результат: список (37, 35, 33, 31, 29).
Правда, что второй операнд в l_range/l_range_r (отчасти) избыточен. Можно было бы ограничиться одним только диапазоном, а для его преобразований явно использовать l_map:
- l_range (Range, Func) – эквивалентно l_map (Func, l_range (Range, ))
- l_range_r (Range, Func) – эквивалентно l_map (Func, l_range_r (Range, ))
Но первый вариант вызова предпочтительней, так как он существенно эффективней (особенно, когда список длинный). И его эффективность – главная причина, ради которой этим функторам можно передать второй (необязательный) аргумент. А первый (т.е. диапазон) может оказаться и пустым (если From >= To), но в этом случае (вполне ожидаемо) результатом тоже будет пустота, т.е. ().
Скажем, вот так можно преобразовать строку в список кодов символов, из которых она состоит:
- ! str_chars (S) = l_range (#$ S, !(I) = s_ord (S, I));
Раз уж разговор зашёл о диапазонах, добавим, что в языке имеются два полезных предиката, проверяющих целое число на принадлежность диапазону:
|
inside (Value, From..To)
|
Истинно, если Value в диапазоне From..To
|
|
outside (Value, From..To)
|
Истинно, если Value вне диапазона From..To
|
Другими словами:
- inside (Value, From..To)
– эквивалентно (From <= Value && Value < To) - outside (Value, From..To)
– эквивалентно (Value < From || Value >= To)
Это всё справедливо, когда диапазон осмысленный (т.е. непустой). Если же он пустой, то inside и outside являются просто синонимами для false и true соответственно.
Функтор l_cmp очень полезен для сравнения двух списков:
|
l_cmp (F_Compare, LeftList, RightList)
|
Последовательно сравнивает списки LeftList и RightList (применяя F_Compare для попарного сравнения элементов) |
Первый операнд для F_Compare должен быть ссылкой на компаратор: бинарный функтор, сравнивающий два своих операнда, и возвращающий целое число. Если это число отрицательное – первый элемент считается "меньшим", чем второй; если оно положительное – первый элемент считается "большим", чем второй; если оно равно нулю – оба элемента считаются "равными". Разумеется, можно использовать и встроенные компараторы: например, cmp – сравнивает два произвольных числа (см. статью 1), а s_cmp – сравнивает две строки "словарно" (см. статью 2). Для менее тривиальных сравнений, можно определить и любой свой собственный компаратор. Сравнение происходит так: списки LeftList и RightList сравниваются поэлементно, до тех пор, пока результат применения F_Compare равен нулю (или пока какой-то список не "кончится"). Если F_Compare вернёт ненулевой результат – процесс завершается, и он им будет результатом всей операции. Если F_Compare вернул нулевое значение для всех пар существующих элементов, результат определяется тем, в каком из списков остались "лишние" элементы. Если LeftList длиннее RightList – возвращается 1; если LeftList короче RightList – возвращается -1; и только если длины списков равны (и все их элементы тоже были попарно "равны") – возвращается 0. Понятно, что сама l_cmp также является компаратором – так что, вложенные списки с её помощью можно сравнивать рекурсивно.
|
l_zip (LeftList, RightList)
|
Поэлементная композиция списков LeftList и RightList
|
Другими словами, l_zip строит список из пар элементов своих списков-операндов, вот так:
- ((LeftList[0], RightList[0]), (LeftList[1], RightList[1]), (LeftList[2], RightList[2]), ... ,)
что альтернативно можно записать и так:
- (LeftList[0]::RightList[0], LeftList[1]::RightList[1], LeftList[2]::RightList[2], ..., )
Результатом всегда является открытый список. Если какой-нибудь из списков-операндов длиннее другого, то результат тоже будет иметь такую длину, просто один из компонентов ("голова" или "хвост") в его элементах будет иметь значение (). Понятно, что если длины списков равны – результат будет иметь ту же длину. Например:
l_zip ((1, 2, 3, 4), ('a', 'b', 'c', 'd'))
– возвращает список:
((1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), )
А применяя l_map к результату l_zip – нетрудно применить попарно к двум спискам произвольный бинарный функтор! Например, l_map (! add, l_zip (LeftList, RightList)) – возвращает поэлементную сумму списков LeftList и RightList (конечно, предполагая, что списки состоят из числовых (или, хотя бы, скалярных) элементов, и они имеют равную длину). Сама l_zip комбинирует только два списка – но её вызовы могут быть вложенными: например, l_zip (ListA, l_zip (ListB, ListC)) – поэлементно комбинирует три списка ListA, ListB и ListC. К каждому из результатов, в свою очередь, может быть применён l_map: это позволяет применить произвольный тернарный функтор к трём списками, или кватернарный к четырём спискам... в общем, дальнейшее понятно.
Этот обзор не исчерпывает всего набора списковых операций (например, не рассмотрена сортировка списков). Но ограничимся пока этим, поскольку статья и так получилась довольно объёмной. И уже со всеми рассмотренными списковыми операциями – можно делать много полезного.
Теги
Лента материалов
Соблюдение Правил конференции строго обязательно!
Флуд, флейм и оффтоп преследуются по всей строгости закона!
Комментарии, содержащие оскорбления, нецензурные выражения (в т.ч. замаскированный мат), экстремистские высказывания, рекламу и спам, удаляются независимо от содержимого, а к их авторам могут применяться меры вплоть до запрета написания комментариев и, в случае написания комментария через социальные сети, жалобы в администрацию данной сети.


Комментарии Правила