Главная мысль урока: деструктуризация описывает форму данных прямо там, где ты даёшь имена. Вектор слева разбирает последовательность по позициям, мапа слева разбирает мапу по ключам. Так функция с первого взгляда говорит, какие части героя ей нужны.
В уроке про мапы герой стал мапой, и функции над ним наполнились вызовами (: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 работает и с вектором, и с мапой, и со строкой, разберём в следующем уроке про абстракцию последовательности.
домашка