Mapeando uma Função nos Valores de um Mapa no Clojure

138

Eu quero transformar um mapa de valores em outro mapa com as mesmas chaves, mas com uma função aplicada aos valores. Eu pensaria que havia uma função para fazer isso na API do clojure, mas não consegui encontrá-la.

Aqui está um exemplo de implementação do que estou procurando

(defn map-function-on-map-vals [m f]
  (reduce (fn [altered-map [k v]] (assoc altered-map k (f v))) {} m))
(println (map-function-on-map-vals {:a "test" :b "testing"} #(.toUpperCase %)))
{:b TESTING, :a TEST}

Alguém sabe se map-function-on-map-valsjá existe? Eu acho que sim (provavelmente com um nome melhor também).

Thomas
fonte

Respostas:

153

Eu gosto muito da sua reduceversão. Eu acho que é idiomático. Aqui está uma versão usando a compreensão da lista de qualquer maneira.

(defn foo [m f]
  (into {} (for [[k v] m] [k (f v)])))
Brian Carper
fonte
1
Eu gosto desta versão porque é super curta e óbvia se você entende todas as funções e coisas que ela usa. E se não o fizer, é uma desculpa para aprendê-los!
Runevault 5/11/2009
Concordo. Eu não conhecia a função into, mas faz todo o sentido usá-la aqui.
Thomas
Oh homem que você não tinha visto? Você está em um deleite. Abuso muito essa função todas as chances que tenho. Tão poderoso e útil.
Runevault 5/11/2009
3
Eu gosto, mas há alguma razão para a ordem dos argumentos (além da pergunta)? Eu estava esperando [fm] mappor algum motivo.
nha
2
Eu recomendo usar (m vazio) em vez de {}. Portanto, continuaria sendo um tipo específico de mapa.
nickik 30/12/19
96

Você pode usar o clojure.algo.generic.functor/fmap:

user=> (use '[clojure.algo.generic.functor :only (fmap)])
nil
user=> (fmap inc {:a 1 :b 3 :c 5})
{:a 2, :b 4, :c 6}
Arthur Edelstein
fonte
Eu sei que essa resposta foi um pouco tarde, olhando para as datas, mas acho que está no local.
precisa
Isso o transformou no clojure 1.3.0?
precisa saber é o seguinte
13
a biblioteca generic.function foi movida para uma biblioteca separada: org.clojure/algo.generic "0.1.0" o exemplo agora deve ser o seguinte: (use '[clojure.algo.generic.functor :only (fmap)]) (fmap inc {:a 1 :b 3 :c 5})
Travis Schneeberger
2
Alguma idéia de como encontrar o fmapClojureScript?
sebastibe
37

Aqui está uma maneira bastante típica de transformar um mapa. zipmap pega uma lista de chaves e uma lista de valores e "faz a coisa certa" produzindo um novo mapa de Clojure. Você também pode colocar as mapteclas para alterá-las, ou ambas.

(zipmap (keys data) (map #(do-stuff %) (vals data)))

ou envolvê-lo em sua função:

(defn map-function-on-map-vals [m f]
    (zipmap (keys m) (map f (vals m))))
Arthur Ulfeldt
fonte
1
Irrita-me que eu tenha que fornecer as chaves, mas não é um preço alto a pagar. Definitivamente, parece muito melhor do que a minha sugestão original.
Thomas
1
Temos a garantia de que chaves e vals retornem os valores correspondentes na mesma ordem? Para mapas classificados e mapas de hash?
Rob Lachlan
4
Rob: sim, chaves e vals usarão a mesma ordem para todos os mapas - a mesma ordem que uma seq no mapa usa. Como os mapas de hash, de classificação e de matriz são imutáveis, não há chance de a ordem mudar nesse meio tempo.
Chouser 5/11/09
2
Parece ser esse o caso, mas está documentado em algum lugar? Pelo menos as instruções para chaves e vals não mencionam isso. Eu ficaria mais confortável usando isso se pudesse apontar para alguma documentação oficial que promete que vai funcionar.
Jouni K. Seppänen 29/03
1
@ Jason Sim, eles adicionaram isso à documentação da versão 1.6, eu acho. dev.clojure.org/jira/browse/CLJ-1302
Jouni K. Seppänen
23

Retirado do livro de receitas de Clojure, há reduzido-kv:

(defn map-kv [m f]
  (reduce-kv #(assoc %1 %2 (f %3)) {} m))
Roboli
fonte
8

Aqui está uma maneira bastante idiomática de fazer isso:

(defn map-function-on-map-vals [m f]
        (apply merge
               (map (fn [[k v]] {k (f v)})
                    m)))

Exemplo:

user> (map-function-on-map-vals {1 1, 2 2, 3 3} inc))
{3 4, 2 3, 1 2}
Siddhartha Reddy
fonte
2
Se não for claro: a função Anon desestrutura a chave e o valor de k e v e, em seguida, retorna um mapeamento de hash-mapa k a (FV) .
Siddhartha Reddy
6

map-map, map-map-keys, Emap-map-values

Não conheço nenhuma função existente no Clojure para isso, mas aqui está uma implementação dessa função map-map-valuesque você é livre para copiar. Ele vem com duas funções intimamente relacionadas map-mape map-map-keystambém estão ausentes na biblioteca padrão:

(defn map-map
    "Returns a new map with each key-value pair in `m` transformed by `f`. `f` takes the arguments `[key value]` and should return a value castable to a map entry, such as `{transformed-key transformed-value}`."
    [f m]
    (into (empty m) (map #(apply f %) m)) )

(defn map-map-keys [f m]
    (map-map (fn [key value] {(f key) value}) m) )

(defn map-map-values [f m]
    (map-map (fn [key value] {key (f value)}) m) )

Uso

Você pode ligar map-map-valuesassim:

(map-map-values str {:a 1 :b 2})
;;           => {:a "1", :b "2"}

E as outras duas funções assim:

(map-map-keys str {:a 1 :b 2})
;;         => {":a" 1, ":b" 2}
(map-map (fn [k v] {v k}) {:a 1 :b 2})
;;    => {1 :a, 2 :b}

Implementações alternativas

Se você deseja apenas map-map-keysou map-map-values, sem a map-mapfunção mais geral , pode usar estas implementações, que não dependem de map-map:

(defn map-map-keys [f m]
    (into (empty m)
        (for [[key value] m]
            {(f key) value} )))

(defn map-map-values [f m]
    (into (empty m)
        (for [[key value] m]
            {key (f value)} )))

Além disso, aqui está uma implementação alternativa map-mapbaseada em clojure.walk/walkvez de into, se você preferir este fraseado:

(defn map-map [f m]
    (clojure.walk/walk #(apply f %) identity m) )

Versões paralelas - pmap-map, etc.

Existem também versões paralelas dessas funções, se você precisar delas. Eles simplesmente usam em pmapvez de map.

(defn pmap-map [f m]
    (into (empty m) (pmap #(apply f %) m)) )
(defn pmap-map-keys [f m]
    (pmap-map (fn [key value] {(f key) value}) m) )
(defn pmap-map-values [f m]
    (pmap-map (fn [key value] {key (f value)}) m) )
Rory O'Kane
fonte
1
Verifique também a função prismática map-vals. É mais rápido usando transientes github.com/Prismatic/plumbing/blob/…
ClojureMostly
2

Como sou um Clojure n00b, é possível que haja soluções muito mais elegantes. Aqui está o meu:

(def example {:a 1 :b 2 :c 3 :d 4})
(def func #(* % %))

(prn example)

(defn remap [m f]
  (apply hash-map (mapcat #(list % (f (% m))) (keys m))))

(prn (remap example func))

A função anon cria uma pequena lista de 2 de cada tecla e seu valor encontrado. O Mapcat executa essa função na sequência das chaves do mapa e concatena todo o trabalho em uma grande lista. "aplicar mapa de hash" cria um novo mapa a partir dessa sequência. O (% m) pode parecer um pouco estranho, é Clojure idiomático por aplicar uma chave a um mapa para procurar o valor associado.

Leitura altamente recomendada: The Clojure Cheat Sheet .

Carl Smotricz
fonte
Pensei em passar por sequências como você fez no seu exemplo. Eu também gosto do nome que você está função muito mais do que minha própria :)
Thomas
No Clojure, palavras-chave são funções que se pesquisam em qualquer sequência que é passada para elas. É por isso que (: keyword a-map) funciona. Mas usar a chave como uma função para procurar em um mapa não funciona se a chave não for uma palavra-chave. Portanto, convém alterar o (% m) acima para (m%), o que funcionará independentemente das chaves.
Siddhartha Reddy
Opa! Obrigado pela dica, Sidarta!
Carl Smotricz
2
(defn map-vals
  "Map f over every value of m.
   Returns a map with the same keys as m, where each of its values is now the result of applying f to them one by one.
   f is a function of one arg, which will be called which each value of m, and should return the new value.
   Faster then map-vals-transient on small maps (8 elements and under)"
  [f m]
  (reduce-kv (fn [m k v]
               (assoc m k (f v)))
             {} m))

(defn map-vals-transient
  "Map f over every value of m.
   Returns a map with the same keys as m, where each of its values is now the result of applying f to them one by one.
   f is a function of one arg, which will be called which each value of m, and should return the new value.
   Faster then map-vals on big maps (9 elements or more)"
  [f m]
  (persistent! (reduce-kv (fn [m k v]
                            (assoc! m k (f v)))
                          (transient {}) m)))
Didier A.
fonte
1

Eu gosto da sua reduceversão. Com uma variação muito pequena, ele também pode reter o tipo de estrutura de registros:

(defn map-function-on-map-vals [m f]
  (reduce (fn [altered-map [k v]] (assoc altered-map k (f v))) m m))

O {}foi substituído por m. Com essa alteração, os registros permanecem registros:

(defrecord Person [firstname lastname])

(def p (map->Person {}))
(class p) '=> Person

(class (map-function-on-map-vals p
  (fn [v] (str v)))) '=> Person

Ao começar {}, o registro perde sua prontidão , que talvez você queira reter, se desejar os recursos de registro (representação de memória compacta, por exemplo).

olange
fonte
0

Estou me perguntando por que ninguém mencionou a biblioteca de espectros ainda. Ele foi escrito para facilitar a transformação desse tipo de código (e, ainda mais importante, o código escrito fácil de entender), apesar de ter um desempenho muito bom:

(require '[com.rpl.specter :as specter])

(defn map-vals [m f]
  (specter/transform
   [specter/ALL specter/LAST]
   f m))

(map-vals {:a "test" :b "testing"}
          #(.toUpperCase %))

Escrever essa função no Clojure puro é simples, mas o código fica muito mais complicado quando você passa para um código altamente aninhado, composto de diferentes estruturas de dados. E é aí que entra o espectro .

Eu recomendo assistir este episódio na TV Clojure, que explica a motivação por trás e os detalhes do espectro .

mzuther
fonte
0

O Clojure 1.7 ( lançado em 30 de junho de 2015) fornece uma solução elegante para isso com update:

(defn map-function-on-map-vals [m f]
    (map #(update % 1 f) m))

(map-function-on-map-vals {:a "test" :b "testing"} #(.toUpperCase %))
;; => ([:a "TEST"] [:b "TESTING"])
Jack Pendleton
fonte