Данные как данные: 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 — это и есть простота.
Проверьте себя
1. Что такое EDN?
AБинарный формат сжатия
BТекстовый формат данных в синтаксисе Clojure
CТип базы данных
DПротокол сети
2. Как правильно достать вложенное значение через get-in?
A(get-in m :a :b)
B(get-in m [:a :b])
C(get-in [:a :b] m)
D(get-in m "a.b")
3. Чем update-in отличается от assoc-in?
AНичем
Bupdate-in применяет функцию к текущему значению, assoc-in ставит готовое значение
Cassoc-in изменяет структуру на месте, update-in копирует
Dupdate-in работает только с векторами