Reputation: 377
I am getting started with Clojure and often finding myself 'materializing' a mapping by making a sequence of key-value pairs and feeding them into a map:
(defn my-function [x] (* x 10))
(def my-function-domain [1 2 3 4 5])
(into {} (map (fn [x] [x (my-function x)]) my-function-domain))
; {1 10, 2 20, 3 30, 4 40, 5 50}
; equivalently
(into {} (map #(vector % (my-function %)) my-function-domain))
Before I put that in a utility function for myself, is there a more idiomatic/performant way to do this?
Thank you!
Upvotes: 2
Views: 137
Reputation: 17859
you could also use something like this:
(update-vals (zipmap my-function-domain my-function-domain) my-function)
;;=> {1 10, 2 20, 3 30, 4 40, 5 50}
you just make a map of {Dom: Dom} and then update each map value with the result of application of f
.
I don't really know whether it is more or less readable than good old map + reduce (or into
in this case), but it shows nice clojure util.
Upvotes: 1
Reputation: 10474
As mentioned zipmap
is idiomatic and concise.
The zipmap
function takes two sequences: one for keys and another for values, and it returns a map where the first sequence provides the keys and the second sequence provides the values.
(zipmap my-function-domain (map my-function my-function-domain))
; => {1 10, 2 20, 3 30, 4 40, 5 50}
If you're looking for an even shorter version, you can leverage the into
function with a map literal combined with a for
comprehension:
(into {} (for [x my-function-domain] [x (my-function x)]))
; => {1 10, 2 20, 3 30, 4 40, 5 50}
Reducing the number of intermediate sequences or collections should, in theory, provide some improvement in performance. Using reduce
can be a direct way of constructing the map without producing intermediate sequences:
(reduce (fn [m x] (assoc m x (my-function x))) {} my-function-domain)
; => {1 10, 2 20, 3 30, 4 40, 5 50}
Here, the reduce function starts with an empty map ({}
) and continually associates the next key-value pair into it.
Even faster can be using transients or java interop. To properly check performance Criterium benchmarking can be used.
Upvotes: 1
Reputation: 6666
There are several ways to approach this. Here are the two approaches I would probably reach for first:
user=> (defn my-function [x] (* x 10))
#'user/my-function
user=> (def my-function-domain [1 2 3 4 5])
#'user/my-function-domain
user=> (zipmap my-function-domain (map my-function my-function-domain))
{1 10, 2 20, 3 30, 4 40, 5 50}
user=> (into {} (map (juxt identity my-function)) my-function-domain)
{1 10, 2 20, 3 30, 4 40, 5 50}
zipmap
is a useful function to produce a hash map from a sequence of keys and a sequence of corresponding values. I think it represents the "materialize" semantics well.
juxt
is a useful function for producing a vector of the result of calling multiple functions on a single value. I use map
with juxt
quite a lot when I want to walk a collection once and produce pairs from each element (and pour the result into a hash map). It's useful, for example, when you have a sequence of hash maps such as a result set from a database query, and you want a hash map from the primary keys to the rows they are in: (juxt :id identity)
.
Note that in my second example map
has just one argument: the function to apply; and into
has three arguments: the "to" collection, the transform to apply, and the "from" collection. This approach uses the concept of transducers to avoid producing intermediate lazy sequences (the zipmap
approach -- and your original code -- produce a lazy sequence of the results of my-function, and then consume that to produce the result). Using the transducer-based approach is often faster than producing intermediate lazy sequences, especially for large sequences.
Upvotes: 3