Clojure: Como posso aplicar uma função a um subconjunto das entradas em um hash-map?
Pergunta
Eu não sou a Clojure e tentando descobrir como fazer isso.
Eu quero criar um novo hash-mapa que para um subconjunto das chaves do hash mapa aplica uma função aos elementos. Qual é a melhor maneira de fazer isso?
(let
[my-map {:hello "World" :try "This" :foo "bar"}]
(println (doToMap my-map [:hello :foo] (fn [k] (.toUpperCase k)))
Este deverá resultar um mapa com algo como
{:hello "WORLD" :try "This" :foo "BAR"}
Solução
(defn do-to-map [amap keyseq f] (reduce #(assoc %1 %2 (f (%1 %2))) amap keyseq))
Breakdown:
Ela ajuda a olhar para ele de dentro para fora. Em Clojure, mistura-mapas agir como funções; se você chamá-los como uma função com uma chave como argumento, o valor associado a essa chave é retornado. Assim, dada uma única chave, o valor atual para essa chave pode ser obtido através de:
(some-map some-key)
Queremos levar valores antigos, e alterá-las para novos valores chamando alguma função f
sobre eles. Assim, dada uma única tecla, o novo valor será:
(f (some-map some-key))
Queremos associar esse novo valor com esta chave em nossa de hash-map "substituindo" o valor antigo. Isto é o que assoc
faz:
(assoc some-map some-key (f (some-map some-key)))
( "Substituir" está em assustar aspas porque não estamos mutação de um único objeto de hash-map, estamos retornando novo, imutável, alterado de hash-map objetos cada vez que nós chamamos assoc
Este ainda é rápido e eficiente. em Clojure porque Maconha mapas são estrutura persistente e share quando você assoc
-los.)
Precisamos assoc
repetidamente novos valores para o nosso mapa, uma tecla de cada vez. Por isso, precisamos de algum tipo de looping construção. O que nós queremos é começar com o nosso hash mapa original e uma única chave, e depois "update" o valor para essa chave. Então tomamos essa nova mistura-mapa ea chave seguinte, e "atualizar" o valor para que a próxima chave. E repetimos isso para cada chave, um de cada vez, e, finalmente, retornar o hash-mapear nós "acumulado". Isto é o que reduce
faz.
- O primeiro argumento para
reduce
é uma função que recebe dois argumentos: um valor "acumulador", que é o valor que mantemos "atualização" mais e mais; e um único argumento usado em uma iteração para fazer alguns dos acumulando. - O segundo argumento para
reduce
é o valor inicial passado como o primeiro argumento para estefn
. - O terceiro argumento para
reduce
é uma coleção de argumentos a ser passado como o segundo argumento para estafn
, um de cada vez.
Assim:
(reduce fn-to-update-values-in-our-map
initial-value-of-our-map
collection-of-keys)
fn-to-update-values-in-our-map
é apenas a declaração assoc
de cima, envolto em uma função anônima:
(fn [map-so-far some-key] (assoc map-so-far some-key (f (map-so-far some-key))))
Assim, colocá-lo em reduce
:
(reduce (fn [map-so-far some-key] (assoc map-so-far some-key (f (map-so-far some-key))))
amap
keyseq)
Em Clojure, há um atalho para escrever funções anônimas: #(...)
é um fn
anônima constituída por um único formulário, no qual %1
é vinculado ao primeiro argumento para a função anônima, %2
para o segundo, etc. Portanto, nossa fn
de acima pode ser escrito de forma equivalente como:
#(assoc %1 %2 (f (%1 %2)))
Isto dá-nos:
(reduce #(assoc %1 %2 (f (%1 %2))) amap keyseq)
Outras dicas
(defn doto-map [m ks f & args]
(reduce #(apply update-in %1 [%2] f args) m ks))
Exemplo chamada
user=> (doto-map {:a 1 :b 2 :c 3} [:a :c] + 2)
{:a 3, :b 2, :c 5}
Espera que isso ajuda.
A seguir parece funcionar:
(defn doto-map [ks f amap]
(into amap
(map (fn [[k v]] [k (f v)])
(filter (fn [[k v]] (ks k)) amap))))
user=> (doto-map #{:hello :foo} (fn [k] (.toUpperCase k)) {:hello "World" :try "This" :foo "bar"})
{:hello "WORLD", :try "This", :foo "BAR"}
Pode haver uma maneira melhor de fazer isso. Talvez alguém pode vir até com uma frase agradável:)