`'~ Макрология 201

Оглавление

  1. "Введение"
  2. Деструктуризация
  3. "Сделай так, чтоб это работало"
  4. Рабочий прототип
  5. Остальные аргументы (&)
  6. Карта по умолчанию (: или)
  7. Сокращенные ассоциативные привязки (: ключи)
  8. Наша первая полная версия
  9. Подведение итогов
  10. Вот драконы
  11. "Использованная литература"
  12. Приложение

Введение

В этом посте мы обсудим, как деструктуризация работает в Clojure, по ходу создавая нашу собственную реализацию. Предполагается, что читатель знаком с Clojure и хоть немного знаком с макросами. Предлагается краткое введение в деструктуризацию, но читателям, незнакомым с этой темой, может быть полезно сослаться на некоторые ссылки внизу этого сообщения, прежде чем продолжить. Если вы чувствуете, что обладаете хорошими практическими познаниями в области деструктуризации, смело переходите к Сделать это работающим.

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

Деструктуризация обеспечивает краткий способ привязки переменных к значениям, отражая форму данных, которые вы хотите связать. Это связано с сопоставлением с образцом и унификацией - концепциями, которые часто встречаются в различных языках программирования и контекстах. Несколько примеров применения этих концепций можно найти в регулярных выражениях (сопоставление с образцом) и логическом программировании (унификация). Поэтому понимание того, как работает деструктуризация, может быть полезно не только для лучшего понимания самого деструктурирования, но и для обеспечения основы для понимания этих связанных концепций.

Одним из таких языков, на котором деструктуризация широко распространена, является Clojure, где он используется с большим эффектом для упрощения процесса связывания переменных в локальных объявлениях, сигнатурах функций, блоках цикла / повторения, для понимания и т. Д. Многие программисты также могут быть знакомы с деструктуризацией. через Javascript, который добавил эту функцию в ES6, помогая сделать ее популярной. Я использовал деструктуризацию на обоих языках и получил удовольствие от нее, поэтому мне стало любопытно узнать о закулисной магии и о том, как можно реализовать упрощенную версию.

Уже есть несколько отличных статей, в которых обсуждается продвинутое использование деструктурирования в Clojure, но я не смог найти ни одной при просмотре реализации. В духе «то, что я не могу построить, я не понимаю» и пытаясь удовлетворить свое любопытство, я открыл исходный код clojure.core, перешел к определению destructure и взглянул на эту небольшую функцию ниже:

Затем я сразу закрыл свой ноутбук и включил серию «Лучше позвони Солу».

По словам Рича Хики: «Может быть, и нет».

Это довольно сложная функция, и для ее усвоения потребуется довольно много времени (по крайней мере, для меня). Возможно, неудивительно, что производственные реализации не всегда являются лучшими источниками для обучения, поскольку эти реализации должны быть связаны с производительностью. Оптимизация, касающаяся эффективности, необходима для кода языка или библиотеки, учитывая частоту использования кода, но часто отвлекает, когда целью является обучение.

Сделайте это, сделайте это правильно, сделайте это быстро

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

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

Посоветовавшись с этими мастерами шепелявления, я решил реализовать самую простую версию деструктуризации, которую я мог придумать, бросив эффективность (и, вероятно, правильность на первом проходе) на ветер.

После краткого обзора деструктуризации (подробный обзор см. В ссылках), мы обратимся к простой реализации, которая будет итеративно уточняться в последующих разделах. Что мне особенно нравится в PAIP Питера Норвига, так это то, что он представляет концепции таким образом, сначала начиная с простого прототипа и последовательно улучшая код в ходе нескольких рефакторингов, показывая каждую версию и решения / компромиссы, принятые на этом пути. Большинство книг пропускают этот процесс, и это досадно, потому что это очень полезно видеть. Часто я читаю книги, показывающие только законченный код, и думаю про себя: «Я никогда не смогу придумать это», и когда я читаю код Норвига, я…. до сих пор так думаю, но, по крайней мере, я вижу, что даже ему нужно с чего-то начинать - окончательный вариант не просто волшебным образом слетает с его кончиков пальцев. Итак, поскольку имитация - самая искренняя форма лести, я воспользуюсь аналогичным подходом здесь, представив сначала очень простой прототип, который будет постепенно улучшаться в ходе нескольких рефакторингов, пока не будет достигнута окончательная версия.

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

В Clojure есть две основные формы деструктуризации: последовательная и ассоциативная. Последовательное деструктурирование применяется к последовательностям и вещам, которые могут стать последовательностями (списки, строки, векторы *, последовательности), ассоциативное деструктурирование применяется к хэш-картам и векторам *.

* Векторы бывают как последовательными, так и ассоциативными. Их можно ассоциативно деструктурировать по их индексам, но обычно они деструктурируются как последовательности.

Последовательная деструктуризация

Абстракция последовательности (ISeq) Clojure обеспечивает последовательный способ навигации по последовательным структурам данных с помощью first, rest, next и cons. На этой абстракции построены дополнительные функции последовательной навигации, например nth, nthrest, last, nthlast и т. Д.

Примечание. Если вы когда-нибудь слышали о термине «интерфейс поставщика услуг» (SPI), упоминаемом в сообществе Clojure, то ISeq - хороший пример такого интерфейса. Любой программист, желающий участвовать в абстракции последовательности, может реализовать четыре функции, требуемые интерфейсом ISeq, а остальные функции последовательностей стандартной библиотеки теперь будут работать с их абстракцией. Этот шаблон вызова общедоступного API более высокого уровня в SPI более низкого уровня распространен в Clojure.

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

Ассоциативная деструктуризация

Основная ассоциативная структура данных в Clojure - это карта. Карты - это функции, которые принимают ключ в качестве аргумента и возвращают значение, связанное с этим ключом. Ключевые слова и символы также являются функциями - при вызове с картой в качестве аргумента и ключевые слова, и символы возвращают значение, соответствующее такому ключевому слову или символу на карте. В каждом случае можно указать необязательный третий аргумент по умолчанию, если на карте не найдено подходящего ключевого слова или символа. Примеры, иллюстрирующие различные способы извлечения значений из карты, показаны ниже, начиная со строки 10. Обратите внимание, что мы могли использовать символы, а не ключевые слова, и результаты были бы такими же.

Стандартный способ деструктуризации карты показан в начале строки 22 выше. У нас есть форма let с парой [binding value], где binding - это карта, ключи которой являются символами, которые мы хотим связать, и чьи значения являются ключами, которые мы будем искать в деструктурируемой карте value (в данном случае карта lispers) . Например:

(let [{paip :paip} lispers]..)

означает привязку символа paip к результату поиска ключа :paip на карте lispers, например (let [paip (lispers :paip)] ...)

Связанные символы не обязательно должны называться так же, как ключи, которые мы ищем - мы могли бы, например, выбрать {p :paip}, который привязывал бы символ p к (lispers :paip). Эта форма связывания, хотя и приятная и декларативная, довольно многословна. Кажется излишним снова и снова повторять названия символов, которые мы хотим связать. К счастью, существует сокращение для ассоциативной деструктуризации, которое показано в строке 42 выше. Если мы ограничиваем содержание, ограничивая символы, которые мы хотим связать, с тем же именем, что и ключи, которые мы хотим найти (например, привязать символ paip к значению (lispers :paip)), тогда это сокращение обеспечивает компактный способ деструктуризации карты. Если, однако, мы хотим присвоить символам другое имя (например, привязать символ p к значению (lispers :paip)), тогда мы должны использовать стандартную форму деструктуризации карты.

Деструктуризация может быть вложенной

Чтобы завершить наш обзор деструктуризации, ниже представлен последний пример, показывающий, что деструктуризация является рекурсивным процессом и, следовательно, может быть вложенной. Представьте себе, сколько шаблонов потребуется для связывания этих символов без деструктуризации. Чтобы было ясно, деструктуризация на любом уровне вложенности сверх этого (и потенциально даже на этом уровне) может привести к чрезмерно плотному коду, но тем не менее это иллюстративно. Для сильно вложенных структур создание нескольких промежуточных привязок с let и деструктуризация этих привязок обычно более читабельны, чем попытки сделать все это за один проход. С большой властью приходит большая ответственность.

Разрушение функциональных аргументов

Наконец, я думаю, стоит упомянуть, что краткость - не единственное преимущество деструктуризации. Разрушение аргументов функции значительно улучшает читаемость. Рассмотрим следующие определения функций:

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

Сделай так, чтоб это работало

Разобравшись с предварительными сведениями, мы можем перейти к основной теме - реализации. Как Clojure переводит эти [bindings, values] пары в соответствующие функции доступа? Мы знаем, что доступ к последовательным данным осуществляется через такие функции, как first, rest и nth. Есть ли способ перевести структуру данных привязки в эти вызовы функций? Тот же вопрос относится к ассоциативной деструктуризации, но с разными функциями доступа, например подходящие для таких карт, как get. Мы хотим преобразовать структуру данных bindings в функции доступа в структуре данных values, при этом функции доступа будут сгенерированы в зависимости от типа структуры bindings.

Как этого добиться? Оказывается, это довольно просто: мы просто просматриваем данные привязки, извлекая нужную нам информацию из значения, которое деструктурируется по ходу дела. Мы выводим вектор привязок, который затем можно вставить в let форму.

Вы можете заметить, что в строках 4 и 12 цитируются данные bindings. Это сделано для того, чтобы Clojure не оценивал данные, которые могли бы включать разрешение символов в таких данных и приводить к ошибке, поскольку эти символы не связаны. Мы хотим манипулировать необработанными данными (вектором символов), а не оцененными данными (вектором символов, представляющим привязки к значениям).

С этим кодом есть несколько проблем, но это только начало. Во-первых, мы не обрабатывали аргументы отдыха для последовательных данных, например & args. Мы также не обрабатывали сокращенные ассоциативные привязки через :keys, карту аргументов по умолчанию :or (также для ассоциативных данных) или :as привязки, которые связывают все деструктурируемое значение. Возможно, более очевидным является тот факт, что эти функции не обрабатывают вложенную деструктуризацию. Что происходит, когда мы пытаемся разрушить следующую последовательность?

Это не сработает, поскольку мы просто просматриваем каждый элемент в последовательностях bindings и values попарно и привязываем все элементы, с которыми мы сталкиваемся, к их соответствующему значению. Когда мы попадаем в 3-й элемент вектора bindings, мы просто объединяем его в пару с 3-м элементом вектора values. Поскольку третий элемент в векторе bindings сам по себе является последовательностью, то, что мы должны были сделать вместо этого, - это деструктурировать этот элемент. Было бы неплохо, если бы у нас был способ деструктурировать последовательности, не так ли?

Мы уже определили, как деструктурировать последовательности, все, что нам нужно сделать, это просто продолжить тот же процесс каждый раз, когда мы сталкиваемся с подпоследовательностью, выполнив рекурсивный вызов.

Та же самая логика применяется к destructure-map, нам нужно проверить, являются ли ключи, которые мы деструктурируем в bindings карте, картами, и рекурсивно вызвать функцию, если они есть.

Эти два изменения - все, что нужно для деструктуризации вложенных последовательностей или вложенных карт. А как насчет карт внутри последовательностей или последовательностей внутри карт? Если во время вызова destruct-sequential мы встретим карту во время навигации по bindings элементам, мы снова вернем ее вместе с соответствующим ей элементом в данных values, не разрушая результаты. Мы вернулись на круги своя. Кажется, что внутри destruct-sequential нам нужно не только знать, является ли элемент в векторе bindings, который мы пересекаем, последовательностью, но и является ли это картой. То же самое и с destruct-associative, недостаточно проверить, является ли каждый элемент картой, мы также должны проверить последовательность.

Рабочий прототип

Общая структура приведенных выше destruct-associative и destruct-sequential выглядит очень похожей, и добавление теста для sequential? и map? к каждой функции кажется излишним. Есть ли способ отделить задачу отправки соответствующей функции деструктуризации от задачи выполнения работы по деструктуризации соответствующей структуры данных?

Наконец-то мы подошли к нашей первой рабочей реализации, обрабатывающей каждую комбинацию последовательностей и карт, вложенных на произвольную глубину. Диспетчеризация осуществляется destruct, который вызывает соответствующую функцию для каждого типа данных, и в результате destruct-sequential и destruct-associative больше не нужно проверять тип данных перед выполнением рекурсивного вызова, они могут просто вызвать destruct.

Одним из новых дополнений является макрос let-, который вызывает destruct для создания последовательности привязок и преобразует эту последовательность в вектор, который нужно отбросить внутри let*. Поскольку let- теперь обрабатывает преобразование последовательности привязок в вектор (строка 33), предыдущие вызовы vec внутри destruct-sequential и destruct-associative больше не нужны. Причина, по которой мы используем let*, а не let, состоит в том, что let определен внутри Clojure для деструктуризации переданных в привязках, let* - это более фундаментальная форма связывания, которая определена до введения деструктурирования в clojure.core. Мы реализуем здесь нашу собственную версию деструктурирования, поэтому мы не хотим, чтобы Clojure выполнял всю работу за нас.

Эта реализация - шаг в правильном направлении, но необходимо решить несколько проблем. Мы до сих пор не обработали &, :or, :keys или :as. В то время как последние два являются скорее вспомогательным слоем, первый, &, является довольно важной частью последовательной деструктуризации, а второй, :or, обеспечивает привязки по умолчанию, аналогичные тому, как функции ключевых слов работают в других языках. Сначала мы исправим эти упущения, начиная с реализации &, а затем по порядку обратим внимание на оставшиеся три.

Остальные аргументы (&)

Как мы должны обрабатывать последовательности деструктуризации, когда мы встречаем элемент & в bindings данных? Кажется, что нынешнее определение destruct-sequential может затруднить это изменение. В настоящее время мы отображаем bindings, по ходу извлекая соответствующий элемент в values. Что произойдет, если мы нажмем & во время вызова map? Нужно ли нам переопределять функцию для выполнения bindings цикла, просматривая один элемент вперед, чтобы мы знали, к какому символу привязать остальные аргументы? Это одно из решений, но для этого потребуется полностью переписать destruct-sequential, чтобы полагаться на ручной обход, а не на то, чтобы map выполнял эту работу за нас. Альтернативное решение можно найти, используя тот факт, что аргументы rest всегда находятся в конце списка аргументов: [x y & zs]. Обе версии представлены ниже:

Оказывается, «полная перезапись» (версия 1) в конце концов не была задействована. Фактически, это выглядит более чистым из двух решений. Мы будем использовать эту версию сейчас, но вернемся к версии 2 при более позднем рефакторинге.

В строке 8 есть тонкая деталь, использование next против rest, которую стоит упомянуть, поскольку это может быть не сразу очевидно, почему различие важно. Основная функция диспетчеризации destruct (см. Строку 5 в dstruct-10.clj) проверяет наличие привязок через (when bindings body), который выполняет тело, только если привязки не равны нулю. Вызов rest в последовательности без дополнительных элементов создает пустой список (), а не nil, и приведет к бесконечной рекурсии, поскольку destruct и destruct-sequential будут вызывать друг друга бесконечно.

Обратите внимание, что мы не можем изменить тест в destruct на стандартную идиому clojure (when (seq bindings) …), потому что bindings может быть символом, который не является последовательным.

Карта по умолчанию (: или)

Затем давайте реализуем :or привязки по умолчанию для ассоциативных данных. Мы хотим иметь возможность предоставлять карту с символами для значений, которые будут использоваться, если значение, которое мы пытаемся привязать, не найдено на карте, которую мы деструктурируем. Это служит той же цели, что и аргументы ключевого слова в таких языках, как python или ruby.

Сначала мы проверяем, предоставлена ​​ли карта по умолчанию в строке 2, и если да, удаляем связанную с ней пару ключ / значение из карты привязки, сохраняя отдельную ссылку на карту по умолчанию. Если мы встречаем значение в binding-map, которого нет в val-map, мы ищем это отсутствующее значение в default-map. Карты по умолчанию ожидают сопоставления символов со значениями, поэтому нам нужно преобразовать отсутствующее значение, которое мы пытаемся найти, от ключевого слова до символа (конец строки 5), чтобы найти совпадающее значение в default-map.

Сокращенные ассоциативные привязки (: ключи)

Теперь мы обратимся к :keys, который обеспечивает сокращенный способ связывания ассоциативных данных, позволяя нам писать {:keys [a b c]} вместо {a :a b :b c :c}. Эта функция может сэкономить значительный объем места при работе со многими ключами, особенно с более длинными именами. Было бы довольно утомительно набирать такие вещи, как:

{first-name :first-name last-name :last-name address :address ….}

снова и снова - :keys наш выход из этого.

Строки 3 и 4 выше - это новые дополнения, остальное не изменилось по сравнению с dstruct-12.clj выше. Если привязки ярлыков присутствуют в деструктурируемой карте, мы преобразуем вектор, связанный с :keys, в хэш-карту {symbols :keys} и действуем, как прежде.

Наша первая полная версия

Последнее изменение, :as привязок, будет позже. На данный момент у нас есть полная версия, обрабатывающая остальные аргументы, аргументы по умолчанию и ассоциативные привязки ярлыков. Ниже мы показываем эту версию, включающую все предыдущие изменения:

Наконец-то готово! Мы внесли довольно много изменений между нашим первым прототипом в dstruct-10.clj и этой версией, поэтому давайте проведем пару тестов на ответе, чтобы убедиться, что все выглядит хорошо.

С destruct все выглядит хорошо, но с let- творится что-то забавное. Некоторые из возвращаемых значений выглядят разумными, но что происходит в строках 31 и 33–35? Давайте посмотрим на макрорасширение для этих вызовов, чтобы увидеть, сможем ли мы понять, что может пойти не так.

Строки-нарушители пронумерованы 1–4 выше, чтобы облегчить сравнение между вызовами макрорасширения и выводом под ними. Начиная с номера один, мы видим в строке 13, что a привязан к range, а b привязан к 10. Что именно здесь происходит? Похоже, что вызов (range 10) в строке 3 не был оценен и вместо этого обрабатывается как цитируемый список для последовательной деструктуризации. Это также объясняет поведение, которое мы наблюдаем в строках 15–17. В каждом случае мы пытаемся связать символы с неоцененным списком (range 10), тогда как вместо этого мы хотим привязать символы к оцененному списку, производящему (0 1 2 3 4 5 6 7 8 9). Что изменилось между нашими тестовыми вызовами на destruct, который, казалось, справился с этим очень хорошо, и нашими вызовами на let-?

Напомним, что для работы destruct первый аргумент, bindings, должен быть заключен в кавычки для подавления оценки. Если мы посмотрим на строку 3 dstruct-15.clj, мы увидим, что первый аргумент действительно цитируется, а второй аргумент - нет. Это имеет смысл, поскольку мы не хотим оценивать bindings форму, но мы хотим оценить values форму. Однако при вызове в let- нам не нужно указывать какие-либо аргументы, поскольку макросы не оценивают свои аргументы. Это означает, что оба аргумента destruct (bindings и values) не вычисляются в let- и, следовательно, не вычисляются, когда деструкция вызывается в let-. Как мы можем решить эту проблему?

Все, что нам нужно, - это одно небольшое изменение в строке 7 - вычислить аргумент values для уничтожения. После внесения этого изменения давайте повторно запустим тесты из dstruct-16.clj выше.

Похоже, мы устранили проблему. Давайте еще раз вызовем let-, а не просто покажем макрорасширение (повторяя тесты, которые мы выполнили в строках 19–27 из dstruct-15.clj выше), и убедимся, что возвращаемые значения кажутся правильными.

Боль никогда не заканчивается. Итак, в чем проблема? Первые два вызова выглядят нормально, но затем мы обнаруживаем ошибку в третьем и нескольких других вызовах. Кажется, что ошибка в каждом случае одна и та же - мы пытаемся где-то рассматривать java.lang.Long как функцию. Если мы снова посмотрим на макрорасширение, связанное с первой строкой, вызывающей ошибку выше (строка 13 из dstruct-18.clj), мы увидим, что оно создает следующий код:

(let* [a 0 b 1 c 2 cs (3 4 5 6 7 8 9)] [a b c cs])

Во время выполнения мы пытаемся привязать cs к списку без кавычек (3 4 5 6 7 8 9), что приводит к ошибке, поскольку 3 не является функцией. Этот список является результатом вызова nthrest при обнаружении & во время последовательной деструктуризации. Вместо этого мы хотим вернуть список, который не будет оцениваться во время выполнения. Это можно сделать двумя способами. Во-первых, мы могли бы изменить destruct-sequential для создания вектора, а не списка при обнаружении &, поскольку векторы будут оценивать сами себя во время выполнения. В качестве альтернативы мы могли бы вернуть цитируемый список, чтобы во время выполнения оценка была подавлена. Реализация деструктуризации в Clojure возвращает списки, поэтому мы выберем последний вариант, чтобы сохранить согласованность, но обе версии представлены ниже.

Бит `'~val-seq означает `(quote ~val-seq), что означает, что возвращаемый результат не будет вычислен во время выполнения, в точности то, что мы хотим. Повторный запуск примеров из dstruct-19.clj теперь дает следующие результаты:

Наконец-то - похоже, у нас есть рабочая версия. Для исправления кода потребовалось всего два небольших изменения:

  • вычислить values аргумент для destruct
  • процитируйте список, возвращенный вызовом nthrest, созданный & в destruct-sequential.

Заключение

Ну, это было немного дольше, чем я думал, прежде чем я начал писать ... Если вы все время задерживались, спасибо, что нашли время, я ценю это! Надеюсь, вы нашли этот пост интересным и информативным. Однако у меня есть к вам один вопрос; Были ли у кого-нибудь из читателей чувство неловкости, когда они читали? Может быть, неприятное ощущение, что что-то кажется не совсем правильным?

Первый пример работает, но по неправильным причинам. Мы оцениваем list-of-things во время расширения макроса, которое фактически разрешается в (range 10 20), поскольку глобальные переменные доступны во время расширения макроса. Однако локальные привязки, например те, которые находятся внутри let, fn, defn, loop, do и т. д., недоступны до времени выполнения, и попытка eval этих привязок приводит к ошибке, как показано выше во втором и третьем примерах.

Последний пример несколько отличается от других, снова показывая, что мы пытаемся использовать java.lang.Long в качестве функции - ту же проблему, которую, как мы думали, мы решили в dstruct-20.clj, вернув список в кавычках. когда nthrest вызывается в результате обнаружения & во время последовательной деструктуризации. Однако в этом случае нет вызова nthrest, список, созданный (range 10), просто возвращается как есть, без кавычек, для оценки времени выполнения. Мы исправили проблему только при строго определенном сценарии. Любой список, возвращаемый как значение привязки вне контекста rest args (который цитирует возвращенный список), вернет нас в ту же ситуацию, в которой мы были раньше. Как мы можем с этим справиться? Должны ли мы проверять тип каждого возвращаемого значения и указывать его, если это список? Кажется, что все выходит из-под контроля.

Вот драконы

Наконец, рассмотрим этот, казалось бы, простой пример:

Наше маленькое умное дополнение к eval принесло отчаяние и разрушение! Мы привязываем a к оценке 10, которая оценивается сама по себе, но затем мы пытаемся привязать b к оценке a, которая еще не связана и не будет до времени выполнения. Последствия ужасающие - вся наша работа до сих пор привела к деструктурирующей форме, которая не может использовать предыдущие привязки и, следовательно, бесполезна. Вернемся и удалим вызов eval? Если мы это сделаем, то снова вернемся к ситуации, когда такие выражения, как (range 10), обрабатываются как списки символов и ненадлежащим образом деструктурируются как последовательности. Похоже, мы зашли в тупик, есть ли выход?

Это будет темой второй части, но вот подсказка:

использованная литература

Основное внимание в этом посте уделялось реализации, а не множеству способов использования API деструктуризации - эта тема уже хорошо изучена, и я не мог представить, что смогу сделать работу лучше, чем два блога, упомянутые ниже. Я ссылался на блог Бруно бесчисленное количество раз за последние несколько лет, и он дает именно то, что написано на банке, - * очень * исчерпывающее руководство по использованию деструктуризации. Второй блог является относительно новым на момент написания этой статьи (июль 2021 г.) и охватывает как использование, так и исследование внутренних механизмов деструктуризации, и будет хорошим дополнением к этой статье. Некоторое время я думал о написании этого поста, и блог Дэниела дал мне дополнительный толчок, который мне был нужен, чтобы наконец сделать это.

[1] Полное руководство по деструктуризации Clojure | Бруно Боначчи

[2] Наблюдение за кодом: разрушение Clojure | Даниэль Грегуар

Кроме того, очень полезными оказались следующие книги, большинство из которых были упомянуты в сообщении:

[3] Парадигмы программирования искусственного интеллекта | Питер Норвиг

[4] О Лиспе | Пол Грэм

[5] Структура и интерпретация компьютерных программ | Хэл Абельсон и Джеральд Сассман

[6] Освоение макроса Clojure | Колин Джонс

Приложение

Ниже представлена ​​полная версия, включающая изменения, которые мы внесли в destruct-17.clj и destruct-20.clj. Это станет отправной точкой для следующего поста, в котором мы исправим код для обработки локальных привязок и, среди прочего, исключим использование eval.