Как ключи карты интерпретируются в Clojure?
Я пытаюсь создать литерал карты с помощью ключей, определенных из случайной функции:
user=> {(str (rand-int 5)) "hello" (str (rand-int 5)) "goodbye"}
IllegalArgumentException Duplicate key: (str (rand-int 5)) clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:71)
тогда как
user=> {(str (rand-int 5)) "hello" (str (rand-int 6)) "goodbye"}
{"4" "hello", "2" "goodbye"}
Похоже, что Reader обрабатывает ключ как список без оценки.
Я не могу найти подробностей об этом в документации. Есть ли кто-нибудь, кто может помочь мне понять это немного больше?
Ответы
Ответ 1
Пройдя через источник компилятора Clojure, я нашел следующее:
-
Здесь класс LispReader
, который содержит вложенный класс MapReader
, который отвечает за чтение картографических литералов. Метод invoke
читает Clojure формы между символами {
, }
и возвращает карту (форм Clojure) вызов RT.map
.
-
RT.map
вызывает PersistentHashMap.createWithCheck
, где выполняется фактическая проверка дублированных ключей . Поскольку мы строим карту форм Clojure, проверка будет срабатывать, даже если есть две одинаковые формы, которые оцениваются по разным значениям (например, в вашем примере).
-
Оценка всех форм Clojure производится в классе Compiler
, в частности, формы карты оцениваются внутри нее вложенными class MapExpr
. Метод eval
оценивает ключи и значения карт и снова создает постоянную карту с помощью RT.map
. Таким образом, проверка дублированных ключей будет выполняться и с оцененными значениями, поэтому следующий код также будет терпеть неудачу:
(let [x :foo y :foo]
{x :bar y :baz}) ;; throws duplicated key exception
Я не уверен, почему авторы решили выполнить проверку дублированных ключей на обеих картах форм и на карте значений. Вероятно, это своего рода "неудачная стратегия": такая реализация будет сообщать об ошибках на ранней стадии компиляции (хотя могут быть ложные срабатывания), и эта проверка не будет дезайсирована для среды выполнения.
Ответ 2
Все, произведенное читателем, не оценено. Это основная идея читателя: он читает формы как данные с минимальной интерпретацией. Читатель дает неоплачиваемую карту компилятору.
Читатель работает, строя карту постепенно с помощью assoc или conj. Однако в прошлом этот подход принес бы еще более странный результат для вашего кода: {(str (rand-int 5)) "goodbye"}
. То есть, применяются обычные ассоциативные правила: добавлены последние пары с ключом-значением. Люди столкнулись с этой проблемой, поэтому теперь читатель выполняет проверку contains?
перед добавлением значений постепенно.
В этой статье более подробно рассматриваются читатели Lisp: http://calculist.org/blog/2012/04/17/homoiconicity-isnt-the-point/
Ответ 3
Вы правы в том, что читатель не оценивает карту.
Помните, что оценка происходит после чтения.
Из Clojure справочная документация для читателей:
Тем не менее, большинство программ Clojure начинаются как текстовые файлы, и задача читателя заключается в анализе текста и создании структуры данных, которую увидит компилятор. Это не просто фаза компилятора. Читатель и представления данных Clojure имеют собственную утилиту во многих тех же контекстах, которые могут использовать XML или JSON и т.д.
Можно сказать, что у читателя есть синтаксис, определенный в терминах символов, а язык Clojure имеет синтаксис, определенный в терминах символов, списков, векторов, карт и т.д. Читатель представлен функцией read, которая читает следующую form (not character) из потока и возвращает объект, представленный этой формой.
Оценщик нуждается в неоцененной карте, чтобы пройти через нее и оценить ее ключи и значения. Если эта карта имеет один и тот же вид более одного раза в качестве ключа, это недопустимый литерал карты.
Вместо этого вы можете использовать функцию hash-map
(hash-map (rand-int 5) "hello"
(rand-int 5) "goodbye")`.
Обратите внимание, что результирующая карта может иметь один или два ключа в зависимости от того, являются ли ключи различными.
Если вы хотите задействовать два ключа, сделайте sth. как
(zipmap (distinct (repeatedly #(rand-int 5)))
["hello" "goodbye"])
Ответ 4
Я не знаю ответа на вопрос о читателе, но более безопасный способ построения этой хэш-карты состоял бы в том, чтобы ключи были разными. Например:
(let [[k1 k2] (shuffle (range 5))]
{k1 "hello" k2 "goodbye"})