Раздел 28 · Clojure

Деструктуризация

junior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Главная мысль урока: деструктуризация описывает форму данных прямо там, где ты даёшь имена. Вектор слева разбирает последовательность по позициям, мапа слева разбирает мапу по ключам. Так функция с первого взгляда говорит, какие части героя ей нужны.

В уроке про мапы герой стал мапой, и функции над ним наполнились вызовами (:hero/name hero). Вот карточка героя для интерфейса «Хроник Пограничья»:

Работает, но читается тяжело: пять раз hero, два раза :hero/position, и чтобы понять, какие поля нужны функции, надо прочитать всё тело. Теперь та же функция с деструктуризацией:

Список аргументов теперь описывает, что функция ждёт: мапу с именем, здоровьем и максимумом, а в ней позицию из двух чисел. Тело осталось про дело. Деструктуризация работает везде, где Clojure связывает имена: в let, в аргументах fn и defn, в loop. Разберём её по частям.

По позициям: вектор слева

Если слева от значения стоит вектор имён, Clojure раскладывает значение по позициям:

  • Лишние значения справа игнорируются, недостающие становятся nil. Ошибки нет.
  • _ это обычное имя, но по соглашению значит «эта позиция мне не нужна».
  • & others собирает остаток в последовательность. Остаток пустой, значит, others равен nil.
  • :as party даёт имя всему значению целиком, чтобы не терять его после разбора.

Такой разбор называется последовательным. Справа может стоять вектор, список, строка, любая последовательность. Мапу так разобрать нельзя, у неё нет позиций.

Разбор вкладывается сам в себя. Отряд это вектор героев, у каждого координаты вектором:

По ключам: мапа слева

Если слева стоит мапа, Clojure достаёт значения по ключам. Самая явная запись: пара «имя ключ».

Писать каждую пару руками скучно, когда имя совпадает с ключом, поэтому есть сокращения:

  • {:keys [hp gold]} берёт ключи :hp и :gold.
  • {:hero/keys [name hp]} берёт квалифицированные :hero/name и :hero/hp и даёт имена без пространства имён. В «Хрониках» ты будешь писать это чаще всего.
  • {:strs [name]} берёт строковые ключи, так приходят данные из JSON.
  • {:syms [name]} берёт ключи-символы, встречается редко.

Нет ключа, значит, имя получит nil. Как и у позиций, без ошибки.

Такой разбор называют ассоциативным.

Значения по умолчанию и целое

:or задаёт значения для отсутствующих ключей, :as даёт имя всей мапе:

Последняя строка показывает ловушку. :or срабатывает, только когда ключа нет. Если ключ есть, а значение nil, ты получишь nil. Если данные приходят снаружи и там бывают пустые поля, подставляй значение явно: (or title "странник").

Заметь ещё одно: в :or пишут имя, а не ключ. {title "странник"}, а не {:hero/title "странник"}.

Вложенный разбор

Мапа слева может содержать вектор, вектор может содержать мапу. Шаблон повторяет форму данных:

Читай шаблон снаружи внутрь. По ключу :hero/position лежит мапа, из неё взять x и y. По ключу :hero/inventory лежит вектор, его первый элемент это мапа, из неё взять :item/id под именем first-id, остальные элементы назвать more.

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

В аргументах функций

Всё, что работает в let, работает в списке аргументов. Ты уже видел card в начале урока. Особенно часто так разбирают пары мапы, когда обходят её через map: каждый элемент мапы это вектор [ключ значение].

Последний приём это именованные аргументы. Хвост после & можно разобрать как мапу, и тогда функция принимает пары «ключ значение»:

Последняя строка работает с Clojure 1.11: вместо пар в такую функцию можно передать готовую мапу. Но в новом коде чаще принимают мапу обычным аргументом, [name {:keys [hp class]}]. Так вызов (make-hero "Ой" opts) не требует apply и выглядит одинаково во всех версиях языка.

Что под капотом

Деструктуризация это не магия компилятора, а макрос: let с шаблоном раскрывается в обычный let с вызовами nth и get. Функция destructure показывает это раскрытие:

(prn (destructure '[[x y] pos]))
;; примерно: [vec__1 pos x (nth vec__1 0 nil) y (nth vec__1 1 nil)]
(prn (destructure '[{:keys [hp] :or {hp 10}} hero]))
;; примерно: [map__2 hero ... hp (get map__2 :hp 10)]

Позиционный шаблон превращается в nth с запасным nil, поэтому недостающие позиции и дают nil. Ассоциативный шаблон превращается в get, а :or в третий аргумент get. Запасное значение get отдаёт только при отсутствии ключа, отсюда и ловушка с nil из раздела выше. Как устроены макросы, разберём в уроке про код как данные.

Попробуй руками

В задаче пять функций для карточки героя и отряда. Тесты проверяют только поведение, но попробуй написать каждую без единого get и без ключевого слова в роли функции: всё через деструктуризацию.

Проверь себя

Задание

Дальше

Деструктуризация разбирает данные известной формы. А как одна и та же функция first работает и с вектором, и с мапой, и со строкой, разберём в следующем уроке про абстракцию последовательности.

домашка

Домашка