Some time ago I needed a function that returns a large map with values generated by many other functions. The most idiomatic way would be:
(defn get-size [data] 123)
(defn get-structure [data] {:potato-shape true :material 123})
(defn get-moons [data] [:foo :bar])
(defn analyze-planet [data]
{:size (get-size data)
:structure (get-structure data)
:moons (get-moons data)})
(the example is just example. In real world the map is larger and more nested)
The other less idiomatic option would be something like:
(defn get-size [planet data] (assoc planet :size 123))
(defn get-structure [planet data] (assoc planet :structure {:potato-shape true :material 123}))
(defn get-moons [planet data] (assoc planet :moons [:foo :bar]))
(-> {} (get-size data) (get-structure data) (get-moons data))
At the end I ended up with something like this:
(defn get-size [data] 123)
(defn get-surface [size data] {:structure {:potato-shape true :material 123} :water 456})
(defn get-moons [structure data] [:foo :bar])
(defn analyze-planet [data]
(let [size (get-size data)
{:keys [structure water]} (get-surface size data)
moons (get-moons structure data)]
{:size size
:structure structure
:water water
:moons moons}))
The reason is that the subsequent functions needed some values from the previous functions so I needed a let
block.
But at the end all three implementations are basically (-> x y z)
.
But I always thought that there might be a better solution. One possible improvement would be to use local-map
from @seancorfield Github (thank you Sean) to replace {:size size :structure structure :water water :moons moons}
but I was thinking about something completely different…
Completely different:
- Hash map is flat (namespaced keywords)
- I’m decoupling where I am
(-> x y z)
by using named steps (by what to do) which is much easier to test - Changing
actions-in-order
can change order / remove steps that are not needed for a specific planet analysis
(ns planet.size)
(defn get-size [data]
{::diameter 123})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns planet.structure.shape)
(defn get-shape [data size-diameter]
{::potato-shape true
::material 222})
(ns planet.structure.water)
(defn get-water [data size-diameter]
{::amount 555})
(ns planet.structure)
(require '[planet.structure.shape :as shape])
(require '[planet.structure.water :as water])
(defn get-structure [data size-diameter]
(merge
(shape/get-shape data size-diameter)
(water/get-water data size-diameter)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns planet.moons)
(defn get-moons [data]
{::material :rock
::count 3})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns planet)
(require '[planet.size :as size])
(require '[planet.structure :as structure])
(require '[planet.moons :as moons])
(require '[clojure.pprint :refer [pprint]])
; next step deciding function
(defmulti analyze-data (fn [action _planet] action))
(defmethod analyze-data :size [_ planet]
(size/get-size (::data planet)))
(defmethod analyze-data :structure [_ planet]
(structure/get-structure (::data planet)
(:size/diameter planet)))
(defmethod analyze-data :moons [_ planet]
(moons/get-moons (::data planet)))
(def actions-in-order
[:size :structure :moons])
; looping function
(defn analyze-planet [data]
(loop [remaining-actions actions-in-order
planet {::valid-data true ; defmethod functions can return :planet/valid-data true false when :data do not make sense
::data data}]
(if (and (::valid-data planet)
(seq remaining-actions))
; loop through another deciding function
(recur (rest remaining-actions)
(merge planet
(analyze-data (first remaining-actions) planet)))
; invalid data / nothing else to do
planet)))
(pprint (analyze-planet "some data"))
; {:planet/valid-data true,
; :planet/data "some data",
; :planet.size/diameter 123,
; :planet.structure.shape/potato-shape true,
; :planet.structure.shape/material 222,
; :planet.structure.water/amount 555,
; :planet.moons/material :rock,
; :planet.moons/count 3}
The new implementation won’t be faster but I think I’ll thank myself a year later when I decide to make a change.
Could you please tell me what do you think?
Thank you.
Jon