nickrobison
nickrobison

Reputation: 150

Building a map with a list of functions in Clojure

I'm relatively new to Clojure, so I'm having trouble wrapping my mind around how to make the following work.

Starting with a Java method (from the reflect package), I want to extract various properties and end up with a map that looks something like the following:

{ :name "test-method
  :return-type "String"
  :public true
}

Since the logic of building the keys can be rather complex, I'd like to chain a series of functions that take the current map and the method object and either modify the map or return it as is. Something like:

(defn build-public 
[acc method] 
(if (is-public? method) (assoc acc :public true) acc))

Which might be called like:

(make-method-map method [build-public build-return-type build-name])

I've tried a couple of different approaches but can't seem to make it work, any suggestions would be greatly appreciated.

Upvotes: 1

Views: 726

Answers (4)

NikoNyrh
NikoNyrh

Reputation: 4138

I like the reduce fns option, but here is an alternative with a bit different function signatures:

(defn make-name  [method] {:name   (str (:a method) "-" (:b method))})
(defn make-other [method] {:a-is-3 (= (:a method) 3)})

(defn make-method-map [method fns]
  (into {} ((apply juxt fns) method)))

(defn make-method-map [method fns]
  (reduce merge ((apply juxt fns) method)))

(defn make-method-map [method fns]
  (apply merge ((apply juxt fns) method)))


(make-method-map {:a 1 :b 2} [make-name make-other])
; {:name "1-2", :a-is-3 false}

juxt returns a vector of function return values, here the acc is not taken as an input argument but instead into or reduce is used to "merge" these hash-maps into the final result. Originally I thought of using comp but these styles of functions aren't that "composable".

Upvotes: 0

madstap
madstap

Reputation: 1552

I would write make-method-map something like this:

(defn method-map [method]
  (-> {}
      (assoc :return-type (get-return-type method))
      (assoc :name (get-name method))
      (cond-> (public? method) (assoc :public? true))))

Note how you can nest cond-> (or as-> or ->>) inside a ->. Putting any of them inside of a ->> or similar might not work as you'd expect, though.

Why that is is left as an exercise to the reader.

Actually I would write the thing like this:

(defn method-map [method]
  (-> {}
      (assoc :return-type (get-return-type method))
      (assoc :name (get-name method))
      (assoc :public? (public? method))))

Or with a literal map if I could get away with it (one step doesn't depend on the next, etc.)

(defn method-map [method]
  {:return-type (get-return-type method)
   :name (get-name method)
   :public? (public? method)})

But that doesn't show off the trick with nesting cond->.

If you find yourself with a vector of functions, then leetwinski's answer is good. Like everything in programming, it depends.

Edit: For a real world example of a vector of functions being used, check out the concept of interceptors in re-frame or pedestal.

Upvotes: 0

leetwinski
leetwinski

Reputation: 17859

the simple way is to apply every function one by one with reduce:

user> (defn make-method-map [method fns]
        (reduce (fn [acc f] (f acc method))
                {} fns))
#'user/make-method-map-2

user> (defn make-name [acc method]
        (assoc acc :name 123))
#'user/make-name

user> (defn make-other [acc method]
        (assoc acc :something "ok"))
#'user/make-other

user> (make-method-map {:a 1 :b 2} [make-name make-other])
;;=> {:name 123, :something "ok"}

Upvotes: 2

Taylor Wood
Taylor Wood

Reputation: 16194

One way to do this is with cond-> which combines the concepts of cond and ->.

(cond-> {:my-map 1}
  (= 2 2) (assoc :two-equals-two true)
  (= true false) (assoc :not-possible "hey"))
=> {:my-map 1, :two-equals-two true}

The clauses on the left determine whether the forms on the right will be evaluated, with the initial value threaded through in the first position.

Upvotes: 0

Related Questions