Список

Список — тип данных в Эрланге, предназначенный для группировки (упорядоченной) нескольких или многих термов, обычно одного типа, в новый, единый терм. То же: list.

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

1> L = [sleep, eat, drink, sleep]. 
[sleep,eat,drink,sleep]

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

2> L2 = [sleep, 8, eat, 1, drink, 0.5, sleep, 2].
[sleep,8,eat,1,drink,0.5,sleep,2]

В этом примере вперемешку с атомами располагаются числа: целые и одно вещественное. Тут как бы подразумевается, что спать мы будем 8 часов, есть — 1 и т.д. Такой список тоже имеет право на существование: в нём после атома действия всегда следует какое-то число. Нам просто остаётся научиться обрабатывать такой список по два элемента — и вуаля! — для нас не будет проблемой то, что заранее количество элементов в списке неизвестно.

На списки очень похожи кортежи. Если списки лучше использовать в тех случаях, когда мы группируем повторяющиеся элементы, причём в неопределённом количестве, то кортежи используют в прочих случаях.

Элементом списка может быть совершенно любой терм. Например, фунтерм или пид. Список это тоже терм, и один список, конечно, может быть элементом другого списка:

1> L1 = [a, b, c]. 
[a,b,c]
2> L2 = [L1, d, e, f]. 
[[a,b,c],d,e,f]

Мы получили список L2, состоящий из четырёх элементов. Их них первый элемент это список [a,b,c].

Для создания пустого списка можно просто написать квадратные скобки без элементов: L = [].

Для перепостроения списков есть очень удобная вещь, называемая обработчиком списка.

Как добавить элементы одного списка в другой

Допустим, мы не желаем, чтобы список вошёл в другой список в качестве единого элемента. Нам надо разложить элементы из одного списка и добавить к элементам другого списка. Делается это с помощью оператора |.

1> L1 = [a, b, c]. 
[a,b,c]
2> L2 = [1, 2, 3 | L1]. 
[1,2,3,a,b,c]

Неправильные списки

С оператором | следует обращаться осторожно. С его помощью легко создать так называемый “неправильный список”.

3> L3 = [1, 2, 3 | 4]. 
[1,2,3|4]

Как мы видим, если с помощью | добавить не переменную, а литерал, тогда получается странный (неправильный) список [1,2,3|4]. С этим неправильным списком можно продолжать работать, снова при этом получая неправильные списки:

5> L5 = [0 | L3]. 
[0,1,2,3|4]

Неправильный список можно подвергнуть декомпозиции:

7> [A,B,C,D|E] = L5.
[0,1,2,3|4]

Голова и хвост списка

Первый элемент списка считается его головой. Остальная часть списка — хвостом.

При работе со списком приходится учитывать особенности работы с памятью. Доступ к голове — очень быстрая операция, поэтому как правило обрабатывают списки, начиная с головы. Это касается как добавления новых элементов — в голову, так и изъястие — из головы.

Частичную декомпозиции списка (получить его голову) можно с помощью того же оператора |, задействовав его в паттерне.

1> L1 = [a, b, c, d, e]. 
[a,b,c,d,e]
2> [H | T] = L1. 
[a,b,c,d,e]
3> H.
a
4> T.
[b,c,d,e]

Паттерн [H | T] обладает, на первый взгляд, неопределённостью. Мало ли где можно поставить разделитель внутри длинного списка. Но на деле он всегда “отрубает” первый элемент. Поэтому мы и использовали такие буквы: H (head — голова) и T (tail — хвост).

Строки как списки чисел

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

Задавая строковые литералы на латинице и кириллице в оболочке, можно заметить различие.

1> Word = "hello". 
"hello"
2> Slovo = "привет". 
[1087,1088,1080,1074,1077,1090]

На самом деле, в обоих случаях мы получаем список из нескольких целых чисел: [104,101,108,108,111] и [1087,1088,1080,1074,1077,1090]. Разница в том, что оболочка в первом случае догадывается, что это текст, а во втором — нет.

Ещё два способа, как задать тот же список, который оболочка воспринимает как “hello”:

3> Word2 = [104, 101, 108, 108, 111]. 
"hello"
4> Word3 = [$h, $e, $l, $l, $o]. 
"hello"

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

5> Slovo2 = 'привет'. 
'привет'
6> is_atom(Slovo2). 
true

Рекурсивная обработка списка

Списки можно (и часто нужно) обрабатывать с помощью рекурсивной функции, то есть функции, которая вызывает саму себя. Типичный подход: в одной кляузе мы с помощью паттерна [H|T] “откусываем” по одному элементу, забирая их из головы списка, что-то делаем с этим элементом, снова внутри этой функции вызываем саму себя, передавая оставшийся хвост списка. Во второй кляузе мы фиксируем тот случай, когда список уже пуст, на этом рекурсия останавливается.

Давайте попрактикуемся в нумерологии и создадим функцию numerolog, которая получает какую-то строку (де факто список чисел, каждое из которых относится к символу юникода), складывает эти числа и возвращает значение.

main() ->
    Text = "Александр Сергеевич Пушкин",
    io:format("~p~n", [numerolog(Text, 0)]).

numerolog([], S) -> S;
numerolog([H|T], S) -> numerolog(T, S+H). 

Наша программа распечатает число 25945.

В данном примере кляузы можно поменять местами — результат будет тот же. Когда функция numerolog/2 получит в качестве аргумента пустой список, шаблон [H|T] на нём не сработает.

Сложение и вычитание списков

Списки можно складывать и вычитать друг из друга — с помощью операторов ++ и --.

1> [a,b,c,d] ++ [d,e,f,g,h].
[a,b,c,d,d,e,f,g,h]
2> "hello world" -- "ledz".
"hlo worl"

Со сложением списка всё очевидно: просто к первому списку “приставляется” второй. С вычитанием надо быть более внимательным. Из второго списка берётся первый элемент и ищется в первом списке. Как только находится, то удаляется. Удаляется только один раз. Чтобы удалить две буквы l в нашем примере, можно было бы написать lledz или ledzl (разницы нет). Потом во втором списке берётся второй элемент и так далее. Если какой-то элемент второго списка не найден в первом, это ни на что не влияет.


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