В предыдущей части мы узнали, что такое формы, как Clojure оценивает их в значения и как возвращать код в виде данных. Теперь мы узнаем о фундаментальной структуре данных, лежащей в основе всего этого: списке.
Смысл существования LISP
Название LISP происходит от “LISt Processor”, и причина этого в том, что все в этом языке на самом деле является списком. То, что мы называем “списком”, на самом деле является структурой данных Linked List, которая представлена в Clojure и всех других исторически сложившихся LISP в виде значений между скобками, разделенных пробелом.
("This" "is" "a" "list")
Как мы видим, формы, составляющие синтаксис языка, представляют собой связный список с функцией в качестве первого элемента, которая применяется ко всем остальным элементам из списка.
Изображение: “Формы как связанные списки”, созданное автором, является нелицензионным материалом с авторским левом.
(str "This " "is" " a " "list")
;; returns => "This is a list"
(apply str (list "This " "is" " a " "list"))
;; returns => "This is a list"
Этот связный список формируется с помощью пар элементов, например, (1 (2 (3 ()))), которые обычно называются cons. В большинстве реализаций LISPs cons – это структура из пары элементов (A.B), в Clojure базовой структурой являются Seqs, которые мы рассмотрим позже, но в языке есть некоторые синтаксические хитрости, поэтому мы также называем их cons, и это то, что мы будем использовать сейчас.
Мы можем создавать списки как создание ячеек cons, каждая из которых получает значение элемента как A, а следующая ячейка как B в (A.B):
(cons "This" (cons "is" (cons "a" (cons "list" '()))))
;; returns => ("This" "is" "a" "list")
Как мы видим, последний элемент каждого списка – это ячейка nil ‘(), которая представляет собой конец списка и игнорируется, когда мы печатаем все элементы, поскольку это просто пустое значение.
Создание списков значений
Поскольку формы являются частью структуры языка, мы не можем просто создать список значений в режиме кода, поскольку Clojure оценивает каждый список как форму.
Если мы попытаемся просто ввести наш список в REPL, то получим ошибку:
("This" "is" "a" "list")
;; returns => Execution error (ClassCastException) at user/eval2051 (REPL:1).
;; => class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'bootstrap')
В большинстве случаев ошибки clojure.lang.IFn возникают из-за того, что что-то связано с функциями, в данном случае из-за того, что “This” не может быть прочитано как функция, поскольку Clojure ожидает, что это будет оператор, который будет применен к остальным элементам.
Поскольку мы создаем списки, нам нужно заключать их в кавычки, чтобы они воспринимались как данные:
'("This" "is" "a" "list")
;; returns => ("This" "is" "a" "list")
Или мы можем вместо этого использовать функцию list, как мы делали в примере выше:
(list "This" "is" "a" "list")
;; returns => ("This" "is" "a" "list")
Поскольку Clojure является динамическим языком, списки могут содержать любое заданное значение:
'(3.1415 "PI" π)
;; returns => (3.1415 "PI" π)
В данном случае мы создали список, содержащий float, string и char.
Получение значений из списка
Главное, что мы должны знать о списках, это то, что когда мы хотим получить значение из списка, мы должны получить доступ ко всем значениям до него. Представьте, что у нас есть следующий список:
'("Brock" "Misty" "Lt. Surge" "Erika")
Если мы хотим получить доступ к “Lt. Surge”, который является третьим элементом, то сначала мы должны получить доступ к двум элементам перед ним “Brock” и “Misty”. Это происходит потому, что это связный список и у нас нет прямой ссылки на позицию каждого значения, так как чем ближе элемент к концу списка, тем больше времени потребуется для получения его значения.
Для получения элементов у нас обычно есть три функции, которые мы комбинируем, чтобы пройти по списку: first, second, last и rest.
первая
Функция first возвращает элемент в начале списка.
(first '("Brock" "Misty" "Lt. Surge" "Erika"))
;; returns => "Brock"
second
Функция second возвращает второй элемент списка.
(second '("Brock" "Misty" "Lt. Surge" "Erika"))
;; returns => "Misty"
last
Функция last возвращает элемент в конце списка.
(last '("Brock" "Misty" "Lt. Surge" "Erika"))
;; returns => "Erika"
rest
Функции rest возвращают список без первого элемента.
(rest '("Brock" "Misty" "Lt. Surge" "Erika"))
;; returns => ("Misty" "Lt. Surge" "Erika")
Получение определенных значений
В большинстве случаев при итерации по спискам мы комбинируем некоторые из этих функций, чтобы достичь нужного элемента. Представим, что мы хотим получить третий элемент.
(first (rest (rest '("Brock" "Misty" "Lt. Surge" "Erika"))))
;; returns => "Lt. Surge"
Когда мы итерируем списки, мы обычно используем рекурсию – это то, что мы увидим позже и что поможет нам не писать много функций для получения данных, но сейчас важно, чтобы вы знали, как их использовать и комбинировать.
Добавление значений
Есть две основные функции, которые мы используем для добавления значений в список. Первая – это функция cons, которая, как мы видели ранее, используется для построения списков, а вторая – conj, которая используется для создания нового списка, добавляя произвольное количество элементов в начало существующего.
(conj '("Koga" "Sabrina" "Blaine" "Giovanni") "Erika" "Lt. Surge" "Misty" "Brock")
;; returns => ("Brock" "Misty" "Lt. Surge" "Erika" "Koga" "Sabrina" "Blaine" "Giovanni")
С другой стороны, когда мы используем cons, мы создаем новый список с переданным точным одним элементом в его начале.
(cons "Brock" '("Misty" "Lt. Surge" "Erika"))
;; returns => ("Brock" "Misty" "Lt. Surge" "Erika")
Вот и все
Теперь вы стали понимать LISP немного больше. В следующих частях мы продолжим изучение некоторых фундаментальных структур данных, которые мы можем использовать для построения наших программ, и как использовать их для структурирования наших данных.