Каков идиоматический способ связать несколько ключей/значений во вложенной карте в Clojure?

Представьте, что у вас есть такая карта:

(def person {
  :name {
    :first-name "John"
    :middle-name "Michael"
    :last-name "Smith" }})

Каков идиоматический способ изменения значений, связанных как с :first-name, так и с :last-name в одном выражении?

(Пояснение: допустим, вы хотите установить :first-name на «Bob» и :last-name на «Doe». Допустим также, что эта карта имеет некоторые другие значения, которые мы хотим сохранить, поэтому создаем ее с нуля не вариант)


person byteclub    schedule 21.12.2010    source источник


Ответы (2)


Вот несколько способов.

user> (update-in person [:name] assoc :first-name "Bob" :last-name "Doe")
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}

user> (update-in person [:name] merge {:first-name "Bob" :last-name "Doe"})
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}

user> (update-in person [:name] into {:first-name "Bob" :last-name "Doe"})
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}

user> (-> person 
          (assoc-in [:name :first-name] "Bob")
          (assoc-in [:name :last-name]  "Doe"))
{:name {:middle-name "Michael", :last-name "Doe", :first-name "Bob"}}

Редактировать

update-in делает рекурсивные assocs на вашей карте. В этом случае это примерно эквивалентно:

user> (assoc person :name 
             (assoc (:name person) 
                    :first-name "Bob" 
                    :last-name "Doe"))

Повторение клавиш становится все более и более утомительным по мере того, как вы углубляетесь в серию вложенных карт. Рекурсия update-in позволяет избежать многократного повторения ключей (например, :name); промежуточные результаты сохраняются в стеке между рекурсивными вызовами. Взгляните на исходный код update-in, чтобы узнать, как это делается.

user> (def foo {:bar {:baz {:quux 123}}})
#'user/foo

user> (assoc foo :bar 
             (assoc (:bar foo) :baz 
                    (assoc (:baz (:bar foo)) :quux 
                           (inc (:quux (:baz (:bar foo)))))))
{:bar {:baz {:quux 124}}}

user> (update-in foo [:bar :baz :quux] inc)
{:bar {:baz {:quux 124}}}

assoc является динамическим (как и update-in, assoc-in и большинство других функций Clojure, которые работают со структурами данных Clojure). Если assoc на карту, он возвращает карту. Если вы assoc переходите к вектору, он возвращает вектор. Просмотрите исходный код для assoc и загляните в RT.java в исходном коде Clojure для получения подробной информации. .

person Brian Carper    schedule 21.12.2010
comment
Спасибо! Как работает синтаксис первого оператора? Откуда assoc знает, что он работает с картой, которая была передана ему при обновлении? Выглядит аккуратно, но как компилятору не запутаться? - person byteclub; 21.12.2010
comment
assoc все равно, если он просто получает карту и некоторые аргументы (пары), тогда он делает свое дело. Карта, которую вы вернете, будет помещена в нужное место семантикой обновления, а затем вернется как карта дыр. - person nickik; 21.12.2010
comment
Не уверен, о чем вы спрашиваете, но я добавил некоторые правки, чтобы расширить ответ, надеюсь, это немного поможет. - person Brian Carper; 21.12.2010
comment
Еще раз спасибо. Я боролся с частью 'assoc :first-name Bob :last-name Doe', пытаясь выяснить, как assoc знает, в какую карту он должен поместить ключи/значения (учитывая, что обычный синтаксис для assoc включает переменную карты до того, как начинаются ключи/значения, такие как (assoc myMap :имя Боб :фамилия Доу) Я собираюсь еще немного поиграть с этим. - person byteclub; 21.12.2010
comment
Как оказалось, все дело в функции «применить», используемой внутри «обновления». Теперь я готов, спасибо, Брайан. - person byteclub; 21.12.2010
comment
Вопрос семилетней давности, но до сих пор актуален. Я попробовал предложенное решение, (update-in person [] assoc :first-name "Bob" :last-name "Doe"), где :first-name и :last-name должны были быть на верхнем уровне, а не вложенными в :name, но новые пары ключ-значение вошли в ключ верхнего уровня nil. Я хотел бы иметь возможность swap! использовать несколько значений в произвольных assoc-in местах, в том числе на верхнем уровне, но я не мог понять, как это сделать ровно одним swap вызовом. - person Sonicsmooth; 08.11.2017
comment
Я сделал так: (defn multi-assoc-in [m & kvvp] (reduce #(apply assoc-in %1 %2) m (partition 2 kvvp))) где kvvp — пары kevvector-value, такие же, как и в assoc-in. Теперь я могу сделать (swap! myatom multi-assoc-in [:a :b] "whatever" [:c :d :e] "blabla") - person Sonicsmooth; 08.11.2017

Я не уверен, что мой случай такой же, но у меня был список изменений, которые нужно применить:

(def updates [[[:name :first-name] "Bob"] 
              [[:name :last-name] "Doe"]])

В этом случае вы можете уменьшить этот список с помощью assoc-in следующим образом:

(reduce #(assoc-in %1 (first %2) (second %2)) person updates)
person Andy    schedule 24.12.2020