Как ключи карты интерпретируются в 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"})