Данные как данные: EDN, get-in и update-in
Учимся обращаться с данными как с данными: формат EDN и навигация по вложенным структурам.
EDN (Extensible Data Notation) — текстовый формат данных Clojure, в котором записываются те же структуры, что и в коде: векторы, map, ключевые слова, строки, числа.
EDN — данные в синтаксисе Clojure
EDN относится к Clojure примерно как JSON к JavaScript, только богаче: помимо чисел, строк и булевых значений в нём есть ключевые слова, множества и символы. Любая структура данных Clojure естественно записывается в EDN.
{:имя "Аня"
:роли #{:admin :user}
:адрес {:город "Москва" :индекс 101000}
:теги ["clojure" "backend"]}Этот же текст — валидный литерал данных в коде Clojure. Поэтому EDN удобен для конфигов (вспомните deps.edn) и обмена данными между сервисами.
Доступ к вложенным данным: get-in
Когда данные вложены друг в друга, доставать значение по одному ключу неудобно. get-in принимает путь — вектор ключей — и спускается по нему:
(def данные {:пользователь {:адрес {:город "Москва"}}})
(get-in данные [:пользователь :адрес :город]) ; => "Москва"
(get-in данные [:пользователь :возраст] :нет) ; => :нет (значение по умолчанию)Вывод:
"Москва" :нет
Неизменяемое обновление: update-in и assoc-in
Менять вложенные данные тоже нужно неизменяемо — возвращая новую структуру. assoc-in задаёт значение по пути, update-in применяет функцию к старому значению по пути:
(def счёт {:баланс {:руб 100}})
; задать новое значение по пути
(assoc-in счёт [:баланс :руб] 500)
; => {:баланс {:руб 500}}
; применить функцию к старому значению
(update-in счёт [:баланс :руб] + 50)
; => {:баланс {:руб 150}}
счёт ; => {:баланс {:руб 100}} (исходное не изменилось)Вывод:
{:баланс {:руб 500}}
{:баланс {:руб 150}}
{:баланс {:руб 100}}Философия «данные как данные»
В Clojure не принято прятать данные за классами и геттерами. Данные путешествуют по программе как обычные map и vector, а функции вроде get-in, update-in, assoc работают с любыми такими структурами единообразно. Это и есть простота: одни и те же инструменты для всех данных, без специальных API под каждый тип записи.
Как работает под капотом
get-in — это просто последовательность get по каждому ключу пути. update-in и assoc-in рекурсивно спускаются по пути и на обратном ходе пересобирают только затронутую ветвь, переиспользуя остальное (структурное разделение из раздела о персистентности). Поэтому глубокое обновление дёшево: копируется лишь путь от корня к изменённому листу.
Частые ошибки
- Забыть, что путь — это вектор.
(get-in m :a :b)неверно; нужно(get-in m [:a :b]). - Ждать мутацию.
update-inвозвращает новую структуру; сохраните результат, исходная не меняется. - Путать
assoc-inиupdate-in. Первый ставит готовое значение, второй применяет функцию к текущему.
Итоги
- EDN — формат данных в синтаксисе Clojure, богаче JSON (keyword, set, символы).
get-inдостаёт значение по пути-вектору ключей, с возможным значением по умолчанию.assoc-inставит значение по пути;update-inприменяет функцию к текущему.- Данные обрабатываются единообразно как обычные map/vector — это и есть простота.