5

I'm working on a small game in Clojure as a learning exercise. I think I've settled on a representation of the game state at any particular time as a list of "movables" and a 2D vector-of-vectors for the "terrain" (board squares).

95% of the time I expect to be checking for a collision in a particular square for which the 2D vector seems appropriate. But in a few cases, I need to go the other direction -- find the (x,y) location of a cell that matches some criteria. First attempt was something like this:

(defn find-cell-row [fn row x y]
  (if (empty? row) nil
    (if (fn (first row)) [x y]
      (find-cell-row fn (rest row) (inc x) y))))

(defn find-cell [fn grid y]
  (if (empty? grid) nil
    (or (find-cell-row fn (first grid) 0 y)
        (find-cell (rest grid) (inc y)))))

(def sample [[\a \b \c][\d \e \f]])
(find-cell #(= % \c) sample 0) ;; => [2 0]

I tried something more concise with map-indexed, but it got ugly quickly and still didn't give me quite what I wanted. Is there a more idiomatic way to do this search, or perhaps I would be better served with a different data structure? Maybe a map { [x y] -> cell }? Using a map to represent a matrix feels so wrong to me :)

kylewm
  • 594
  • 6
  • 21
  • 1
    You could use a Map however one of the advantages of using say an immutable data structure like cons-cell is that it makes it easy to do a MINI-MAX (http://en.wikipedia.org/wiki/Minimax) like algorithm since any operation will essentially clone the board. On the other hand I find car/consing through cells annoying and usually resort to some indexed structure (array or map). – Adam Gent Apr 17 '12 at 21:28
  • It looks like Clojure has nice support for "editing" a cell (i.e., creating a new immutable structure with the value changed) with assoc-in. No AI in this game, but I do want to be able to "rewind" to a previous time so the immutable structs are really handy. – kylewm Apr 17 '12 at 21:58
  • 1
    not sure what you mean about `assoc-in`, but "ordinary" maps in clojure are implemented as functional (immutable) trees, so when you modify a map you get a new instance that shares much structure with previous instances. i used map {[x y] -> cell} for a structure in a dfs and it worked fine. however, it felt very "odd" so i am bookmarking this question to see if there is anything better... – andrew cooke Apr 17 '12 at 22:23
  • http://clojuredocs.org/clojure_core/clojure.core/assoc-in. so I can call `(assoc-in sample [0 2] \z)` to create a copy of sample with the \c changed to a \z. Thanks both of you for the feedback. – kylewm Apr 17 '12 at 22:48
  • assoc-in is something weird. just basic assoc does what i described, which i think is what you want (i think?!) – andrew cooke Apr 17 '12 at 23:36
  • 1
    assoc-in allows a path into a nested data structure. It will create missing intermediate nodes on the fly, so (assoc-in {} [:a :b :c] 3) => {:a {:b {:c 3}}} – sw1nn Apr 17 '12 at 23:49

2 Answers2

4

A nested vector is pretty normal for this sort of thing, and it's neither hard nor ugly to scan through one if you use a for comprehension:

(let [h 5, w 10]
  (first
   (for [y (range h), x (range w)
         :let [coords [y x]]
         :when (f (get-in board coords))]
     coords)))
amalloy
  • 75,856
  • 7
  • 133
  • 187
  • Brilliant, thank you. Playing with that just a little I think I've got a solution I'm really happy with `(defn find-cell [pred s] (first (for [[y row] (map-indexed vector s) [x cell] (map-indexed vector row) :when (pred cell)] [x y])))` This language is so dang fun – kylewm Apr 18 '12 at 01:03
  • I assume you're doing these `map-indexed` things for efficiency or something? I really recommend you profile or benchmark before you make your code more complicated in order to gain speed - I'm pretty sure `new`ing up all those temporary collections costs a lot more than indexing into a vector. – amalloy Apr 18 '12 at 01:29
  • Not at all... Just personal preference. I use Iterables instead of for(int i = 0...) in Java too. – kylewm Apr 18 '12 at 04:34
2

How about using a plain vector then all the 'usual' functions are available to you and you can extract [x y] as necessary.

(def height 3)
(def width 3)

(def s [\a \b \c \d \e \f \g \h \i])

(defn ->xy [i]
    [(mod i height) (int (/ i height))])

(defn find-cell 
    "returns a vector of the [x y] co-ords of cell when
     pred is true"
    [pred s]
    (let [i (first (keep-indexed #(when (pred %2) %1) s))]
      (->xy i)))

(find-cell #(= \h %) s)
;=> [1 2]

(defn update-cells 
    "returns an updated sequence s where value at index i
     is replaced with v. Allows multiple [i v] pairs"
    [s i v & ivs]
    (apply assoc s i v ivs))

(update-cells s 1 \z)
;=> [\a \z \c \d \e \f \g \h \i]

(update-cells s 1 \p 3 \w)
;=> [\a \p \c \w \e \f \g \h \i]
sw1nn
  • 7,070
  • 1
  • 21
  • 35
  • any idea how this compares to maps for data sharing with copies? if you're making lots of small changes in a process that might backtrack which is more memory-efficient? i suspect both chunk things in groups of 32, but don't have any numbers. – andrew cooke Apr 17 '12 at 23:44
  • structural sharing applies across all the persistent data structures AFAIK, not sure about low-level details for vector vs map. Other benefit is that indexed access to vector is optimized – sw1nn Apr 17 '12 at 23:47
  • Thanks! That makes a lot of sense, and I've learned a couple additional things looking at your example :) – kylewm Apr 18 '12 at 00:07
  • No problem. See http://stackoverflow.com/questions/6016271/why-is-it-better-to-have-100-functions-operate-on-one-data-structure-than-10-fun for further rationale about using the simple data structures... – sw1nn Apr 18 '12 at 00:17