Reputation: 149
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
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
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