I'm remaking an old program of mine in Clojure. It involves "Beings" (generic entities capable of attacking and breeding) let loose in a confined space.

I've noticed a pattern of taking a record, creating a modified version of one of its fields, then "overwriting" the field.

It generally seems to look like:

(defn map-pts [p1 p2 f]
  (let [[x1 y1] p1
        [x2 y2] p2]
    [(f x1 x2) (f y1 y2)]))

(defn add-pts [p1 p2]
  (map-pts p1 p2 +))

(defrecord Being [position, health])

(defn hurt [being damage]
  (let [newHealth (- (:health being) damage)]           ;<<<
    (assoc being :health newHealth)))                   ;<<<

(defn move-by2 [being x y]
  (let [new-position (add-pts (:position being) [x y])] ; <<<
    (assoc being :position new-position)))              ; <<<

(I'm only including the first 2 functions so it's complete).

I decided to write a function to try and limit the boilerplate.

(defn update-with [m k k1->k2]
  (let [oldValue (get m k)]
    (assoc m k (k1->k2 oldValue))))

which can be used like:

(defn hurt2 [being damage]
  (update-with being :health
                 #(- % damage)))

(defn move-by [being x y]
  (g/update-with being :position
                 #(add-pts % [x y])))

which I find to be significantly cleaner.

What I want comments on:

  • Am I thinking about this is the right way? Is this something I should be worrying about, or is this an xy problem?

  • Is k1->k2 an appropriate name? It seems like a very self-documenting way of indicating what type of function is needed, but is also "Hungarian", which I know is frowned upon.

  • This seems like something that would be included in the langage given its simplicity. Does this function already exist?

An example of how it will be used (non-functioning code, since some components aren't yet defined):

(def beings ['(all the beings in the world)])

(map (fn [being]
         (if (is-contacting-other? being beings)
           (hurt being 20)                        ; <<---

In reality, I'll be giving the job of calling hurt to a helper function attack that will decide how much damage to do. This same loop will also handle other things, so it will be a little more complex. This is just a stripped-down example.

  • \$\begingroup\$ Could you add some example code that calls the functions you've defined? \$\endgroup\$
    – Sam Estep
    May 27, 2016 at 2:06
  • \$\begingroup\$ @SamEstep I haven't actually gotten that far yet, but I can whip up an example of how it will be used with non-functioning code. Give me a couple seconds. \$\endgroup\$ May 27, 2016 at 11:32
  • \$\begingroup\$ I understand the high-level idea of how you plan to use these functions, but I'd like to have some concrete, runnable examples to give me a simple place to start playing with your code and testing my changes. As I'm guessing you know, on Code Review it's encouraged to provide working code, not hypothetical code. \$\endgroup\$
    – Sam Estep
    May 27, 2016 at 12:45
  • \$\begingroup\$ Yes. As I mentioned though, I don't yet have any runnable code that uses this unfortunately. I wrote it because I knew from experience that I'd need such a function, but it will be used in my main update loop, which I'm still a ways from being able to write, My question has less to do with this specific use case though, and more to do with how records should be updated in general. \$\endgroup\$ May 27, 2016 at 12:48
  • \$\begingroup\$ It doesn't matter if you don't yet have runnable code; write some anyway. If you want your code to be reviewed, write some test cases that other people can run. \$\endgroup\$
    – Sam Estep
    May 27, 2016 at 14:27

1 Answer 1

  1. I don't think trying to cut down boilerplate like that is too much of an issue.
  2. Normally I would have no problem with using something->somethingelse for the name of a function that converts from something to something else. I'm not sure in this case though. For one thing, I'd assume k means key. k1->k2 kind of suggests it's acting on the key, but it's not. In this case I'd probably just follow the example of the stdlib and call it f. Makes it obvious it is the function that does the updating. (Semi-related: you might want to check out the sample chapter of elements of clojure - it goes into naming things in clojure in some depth)
  3. If you're running clojure 1.7+, you've got the update function that does this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.