Clojure: come posso applicare una funzione a un sottoinsieme delle voci in una mappa hash?
Domanda
Non sto per Clojure e sto tentando di capire come farlo.
Voglio creare una nuova mappa hash che per un sottoinsieme delle chiavi nella mappa hash applica una funzione agli elementi. Qual è il modo migliore per farlo?
(let
[my-map {:hello "World" :try "This" :foo "bar"}]
(println (doToMap my-map [:hello :foo] (fn [k] (.toUpperCase k)))
Questo dovrebbe quindi risultare in una mappa con qualcosa come
{:hello "WORLD" :try "This" :foo "BAR"}
Soluzione
(defn do-to-map [amap keyseq f] (reduce #(assoc %1 %2 (f (%1 %2))) amap keyseq))
Breakdown:
Aiuta a guardarlo dentro e fuori. In Clojure, le mappe hash si comportano come funzioni; se li chiamate come una funzione con una chiave come argomento, viene restituito il valore associato a quella chiave. Quindi, data una singola chiave, il valore corrente per quella chiave può essere ottenuto tramite:
(some-map some-key)
Vogliamo prendere vecchi valori e cambiarli in nuovi valori chiamando una funzione f
su di essi. Quindi, data una sola chiave, il nuovo valore sarà:
(f (some-map some-key))
Vogliamo associare questo nuovo valore a questa chiave nella nostra mappa hash, " sostituzione " il vecchio valore. Questo è ciò che assoc
fa:
(assoc some-map some-key (f (some-map some-key)))
(" Sostituisci " è tra virgolette perché non stiamo mutando un singolo oggetto hash-map; restituiamo oggetti hash-map nuovi, immutabili, alterati ogni volta che chiamiamo assoc
. Questo è ancora veloce ed efficiente in Clojure perché le mappe hash sono persistenti e condividono la struttura quando le
Dobbiamo ripetutamente assoc
nuovi valori sulla nostra mappa, una chiave alla volta. Quindi abbiamo bisogno di una sorta di costrutto loop. Ciò che vogliamo è iniziare con la nostra hash-map originale e una sola chiave, quindi "aggiorna". il valore per quella chiave. Quindi prendiamo quella nuova mappa hash e la chiave successiva e " aggiorna " il valore per quella chiave successiva. E lo ripetiamo per ogni tasto, uno alla volta, e infine restituiamo la mappa hash che abbiamo "accumulato". Questo è ciò che fa riduci
.
- Il primo argomento per
ridurre
è una funzione che accetta due argomenti: un " accumulatore " valore, che è il valore che manteniamo " aggiornamento " ancora ed ancora; e un singolo argomento usato in una iterazione per fare un po 'di accumulo. - Il secondo argomento da
ridurre
è il valore iniziale passato come primo argomento a questofn
. - Il terzo argomento da
ridurre
è una raccolta di argomenti da passare come secondo argomento a questofn
, uno alla volta.
(reduce fn-to-update-values-in-our-map
initial-value-of-our-map
collection-of-keys)
fn-to-update-valori-in-our-map
è solo l'istruzione assoc
dall'alto, racchiusa in una funzione anonima:
(fn [map-so-far some-key] (assoc map-so-far some-key (f (map-so-far some-key))))
Quindi collegandolo a riduci
:
(reduce (fn [map-so-far some-key] (assoc map-so-far some-key (f (map-so-far some-key))))
amap
keyseq)
In Clojure, esiste una scorciatoia per scrivere funzioni anonime: # (...)
è un fn
anonimo costituito da un singolo modulo, in cui % 1
è associato al primo argomento alla funzione anonima, % 2
al secondo, ecc. Quindi il nostro fn
può essere scritto in modo equivalente come:
#(assoc %1 %2 (f (%1 %2)))
Questo ci dà:
(reduce #(assoc %1 %2 (f (%1 %2))) amap keyseq)
Altri suggerimenti
(defn doto-map [m ks f & args]
(reduce #(apply update-in %1 [%2] f args) m ks))
Esempio di chiamata
user=> (doto-map {:a 1 :b 2 :c 3} [:a :c] + 2)
{:a 3, :b 2, :c 5}
Spero che questo aiuti.
Sembra funzionare quanto segue:
(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"}
Potrebbe esserci un modo migliore per farlo. Forse qualcuno può inventare un bel one-liner :)