Kenneth Allen
Kenneth Allen

Reputation: 377

What's the clojurific way to materialize a function into a map?

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

Answers (3)

leetwinski
leetwinski

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

user2609980
user2609980

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

Sean Corfield
Sean Corfield

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

Related Questions