Saytiras
Saytiras

Reputation: 149

Idiomatic way to handle datastructures packed inside a map?

Lets assume we have a data structure that describes a moving ball.

(def moving-ball {:position [100 100]
                  :velocity [1 3]
                  :acceleration [0 0]})

Is it idiomatic to write an update-fn that destructures the map, updates the properties and returns a new map? Like that:

(defn update-acceleration
  [{:keys [position] :as s} mx my]
   (let 
     [[x y] position
     dx (- mx x)
     dy (- my y)
     dir (normalize [dx dy])]
      (assoc s :acceleration dir)))

Or is it better to seperate the functions?

(defn direction
 [[x y] mx my]
 (let [dx (- mx x)
       dy (- my y)]
  (normalize [dx dy])))


(defn update-map
 [{:keys [position] :as s} mx my]
 (assoc s :acceleration (direction position mx my)))

The first is coupled to the data structure (and thus not really reusable) but the second requires more code. Is there a better way to do that?

Upvotes: 3

Views: 133

Answers (2)

Thumbnail
Thumbnail

Reputation: 13473

If we improve the way that the data is presented to the functions, we can usefully extract function direction from entanglement with the map data structure. This is related to what the refactoring folks call introduce (or extract) parameter object and extract method.

The problem is the uneven way that [x y] co-ordinates are presented: sometimes as two numbers, sometimes as one pair. If we represent all of them as pairs, we can get a better grip on what the functions are doing.

If we do this to the direction function, it becomes ...

(defn direction [[x y] [mx my]]
 (let [dx (- mx x)
       dy (- my y)]
  (normalize [dx dy])))

... which we can reduce to ...

(defn direction [from to] (normalize (mapv - to from)))

Now we have a function that we can understand at sight. As it's likely to find use outwith update-acceleration as well as within it, it's viable. (The same function with meaningless argument names was not so convincing).

In the same spirit, we can reform update-acceleration:

(defn update-acceleration [{:keys [position] :as s} [mx my]]
   (let 
     [[x y] position
     dx (- mx x)
     dy (- my y)
     dir (normalize [dx dy])]
      (assoc s :acceleration dir)))

... which reduces to ...

(defn update-acceleration [s m]
    (assoc s :acceleration (normalize (mapv - m (:position s)))))

... or, employing the direction function, ...

(defn update-acceleration [s m]
  (assoc s :acceleration (direction (:position s) m)))

You would get some benefit from so refactoring in any language. Clojure's sequence library amplifies the effect. (Other sequence libraries are available: any Lisp or other functional language, Smalltalk, C#, ... YMMV)


P.S.

I am guessing that normalize returns a unit vector in the same direction. Here's one way to do it using the sequence functions:

(defn normalize [v]
  (let [length (->> v
                  (map #(* % %))
                  (reduce +)
                  Math/sqrt)]
    (mapv #(/ % length) v)))

This is definitely worth extracting. In fact, I'd be tempted to pull out length:

(defn length [v]
  (->> v
       (map #(* % %))
       (reduce +)
       Math/sqrt))

... and define normalize as ...

(defn normalize [v]
  (let [l (length v)] (mapv #(/ % l) v)))

A wee warning: mapv will stop silently on its shortest argument, so you might want to check that all its arguments are the same length.

Upvotes: 3

WeGi
WeGi

Reputation: 1926

Idiomatic would be to use update-in. See http://clojuredocs.org/clojure_core/clojure.core/update-in

update-in would then use the function you called direction. The Function update-in takes the old value of moving-ball and apsses it as an argument to the update-function. So you would have to change direction accordingly.

Upvotes: 1

Related Questions