Clojure: работа с java.util.HashMap в идиоматической манере Clojure

У меня есть объект java.util.HashMap m (возвращаемое значение из вызова кода Java), и я хочу получить новую карту с дополнительной парой ключ-значение.

Если бы m была картой Clojure, я мог бы использовать:

(assoc m "key" "value")

Но попытка сделать это на HashMap дает:

java.lang.ClassCastException: java.util.HashMap нельзя преобразовать в clojure.lang.Associative

Не повезло и с seq:

(assoc (seq m) "key" "value")

java.lang.ClassCastException: clojure.lang.IteratorSeq нельзя преобразовать в clojure.lang.Associative

Единственный способ, которым мне удалось это сделать, - это использовать собственный put HashMap, но он возвращает void, поэтому я должен явно вернуть m:

(do (. m put "key" "value") m)

Это не идиоматический код Clojure, к тому же я изменяю m вместо создания новой карты.

Как работать с HashMap в стиле Clojure?


person foxdonut    schedule 03.11.2009    source источник
comment
Я не думаю, что это возможно. HashMap в Java не является постоянной структурой данных, поэтому либо вы изменяете ее, либо клонируете и модифицируете клонированную версию.   -  person copumpkin    schedule 03.11.2009


Ответы (4)


Clojure делает коллекции java последовательными, поэтому вы можете напрямую использовать функции последовательности Clojure в java.util.HashMap.

Но assoc ожидает clojure.lang.Associative, поэтому вам нужно сначала преобразовать java.util.HashMap в него:

(assoc (zipmap (.keySet m) (.values m)) "key" "value")

Изменить: более простое решение:

(assoc (into {} m) "key" "value")
person Siddhartha Reddy    schedule 03.11.2009
comment
Но тогда у вас нет хэш-карты, у вас есть карта clojure, которая, похоже, не соответствует цели, к которой он стремится. - person Runevault; 03.11.2009
comment
Итак, (сопоставить (в {} m) значение ключа) вместо (сопоставить значение ключа m). Красивый! Спасибо. - person foxdonut; 03.11.2009
comment
Я считаю, что если вам снова понадобится HashMap, вы можете просто создать новый. (java.util.HashMap.m) - person Eric Normand; 24.11.2009
comment
Недавно я использовал этот метод для больших данных и обнаружил, что он очень медленный для вложенных карт и т. д. В конце концов я просто нашел библиотеку clojure, чтобы мне никогда не приходилось иметь дело с картами Java. - person sandos; 21.08.2012

Если вы взаимодействуете с кодом Java, вам, возможно, придется стиснуть зубы и сделать это способом Java, используя .put. Это не обязательно смертный грех; Clojure предоставляет вам такие вещи, как do и ., специально для того, чтобы вы могли легко работать с кодом Java.

assoc работает только со структурами данных Clojure, потому что много работы ушло на то, чтобы сделать создание новых (неизменяемых) копий с небольшими изменениями очень дешевым. Java HashMaps не предназначены для работы таким же образом. Вам придется клонировать их каждый раз, когда вы вносите изменения, что может быть дорогостоящим.

Если вы действительно хотите выбраться из страны мутаций Java (например, вы храните эти HashMaps в течение длительного времени и не хотите, чтобы вызовы Java были повсюду, или вам нужно сериализовать их через print и read, или вы хотите работать с ними потокобезопасным способом с помощью Clojure STM), вы можете достаточно легко конвертировать между Java HashMaps и хэш-картами Clojure, потому что структуры данных Clojure реализуют правильные интерфейсы Java, поэтому они могут взаимодействовать друг с другом.

user> (java.util.HashMap. {:foo :bar})
#<HashMap {:foo=:bar}>

user> (into {} (java.util.HashMap. {:foo :bar}))
{:foo :bar}

Если вам нужна вещь, похожая на do, которая возвращает объект, над которым вы работаете, когда вы закончили работу над ним, вы можете использовать doto. Фактически, Java HashMap используется в качестве примера в официальной документации для этой функции, что является еще одним признаком того, что это не конец света, если вы используете объекты Java (разумно).

clojure.core/doto
([x & forms])
Macro
  Evaluates x then calls all of the methods and functions with the
  value of x supplied at the front of the given arguments.  The forms
  are evaluated in order.  Returns x.

  (doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))

Некоторые возможные стратегии:

  1. Ограничьте свои мутации и побочные эффекты одной функцией, если можете. Если ваша функция всегда возвращает одно и то же значение при одних и тех же входных данных, внутри она может делать все, что захочет. Иногда изменение массива или карты является наиболее эффективным или простым способом реализации алгоритма. Вы по-прежнему будете пользоваться преимуществами функционального программирования до тех пор, пока вы не "сливаете" побочные эффекты остальному миру.

  2. Если ваши объекты будут использоваться какое-то время или они должны хорошо сочетаться с другим кодом Clojure, постарайтесь поместить их в структуры данных Clojure как можно скорее и в последнюю секунду преобразовать их обратно в Java HashMaps (при подаче их обратно на Java).

person Brian Carper    schedule 03.11.2009
comment
Спасибо за подробное объяснение. Очень полезно и интересно. Я изучил совет по doto вместо do, более элегантного способа вызова методов void для объектов Java и получения их обратно в конце. Можно подумать, я это понял, раз я читаю книгу Стью ;-) - person foxdonut; 03.11.2009
comment
Кстати, в Clojure 1.7 (2015 г.) и, возможно, в некоторых более ранних версиях java.util.HashMap теперь даже выводится как Clojurely: (java.util.HashMap. {:a 1 :b 2}) печатается в ответе как {:a 1, :b 2}, хотя это все еще java.util.HashMap. например assoc не будет работать на нем. - person Mars; 07.12.2015

Совершенно нормально использовать хеш-карту Java традиционным способом.
(do (. m put "key" "value") m)
This is not idiomatic Clojure code, plus I'm modifying m instead of creating a new map.

Вы изменяете структуру данных, которая действительно предназначена для изменения. Хеш-карте Java не хватает структурного разделения, что позволяет эффективно копировать карты Clojures. Обычно идиоматический способ сделать это — использовать функции java-interop для работы с java-структурами типичным для java способом или чисто преобразовать их в структуры Clojure и работать с ними функциональным способом Clojure. Если, конечно, это не упростит жизнь и не приведет к улучшению кода; тогда все ставки сняты.

person Arthur Ulfeldt    schedule 03.11.2009

Это некоторый код, который я написал с использованием хэш-карт, когда пытался сравнить характеристики памяти версии clojure и java (но использовал из clojure)

(import '(java.util Hashtable))
(defn frequencies2 [coll]
    (let [mydict (new Hashtable)]
      (reduce (fn [counts x]
            (let [y (.toLowerCase x)]
              (if (.get mydict y)
            (.put mydict y (+ (.get mydict y) 1))
            (.put mydict y 1)))) coll) mydict))

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

person Runevault    schedule 03.11.2009