0
votes

I'd like to create a hash-map that has n number of key-value pairs created in sets of 3 where the sets do not intersect, e.g. [(34 false) (35 false) (36 false)] && [(24 false) (25 false) (26 false)] -> {34 false 35 false 36 false 24 false 25 false 26 false}


EDIT:

To play/practice with Clojure, I'm attempting to implement an idiomatic version of the battleship board game. I decided to store the battleship coordinates in a hash-map where the keys are coordinates and the values are booleans indicating whether that section of the ship has been hit. The specific piece of code below is supposed to

  1. Select an axis (horizontal or vertical)
  2. Select a coordinate for the bow of the ship
  3. "Build" the rest of the ship (3 coordinates in total) by increasing the x or y value accordingly, e.g. {"10" false "11" false "12" false}. Note the "10" translates into the second row of a matrix, first column.
  4. Note: Before adding the ship to the hash-map of coordinates the new ship coordinates must be checked to ensure that an intersection does not exist. If it does, the ship must be "re-built."

To that end, I've created the code below. It has 2 issues:

  1. Executing the function results in the following exception from the use of the 'acc' accumulator:

    clojure.lang.LazySeq cannot be cast to clojure.lang.Associative

  2. The result of the function is not a single hash-map, but rather a list of n hash-maps

Using idiomatic clojure, how can I achieve my goal?

(defn launch
  [n]
  (loop [cnt n acc {}]
    (if (= cnt 0)
      acc
    (recur
      (- cnt 1)
      ((fn []
        (let [axis (rand-int 2)]
            (if (= axis 0)
              (let [x (rand-int 8) y (rand-int 10)]
                (for [k (range 3)]
                  (assoc acc (str y (+ x k)) false)))
              (let [x (rand-int 10) y (rand-int 8)]
                (for [k (range 3)]
                  (assoc acc (str (+ y k) x) false)))))))))))
2

2 Answers

3
votes

that's how i would rewrite it:

(defn create-key [axis-val i]
  (if axis-val
    (str (rand-int 10) (+ (rand-int 8) i))
    (str (+ (rand-int 8) i) (rand-int 10))))

(defn launch [n]
  (reduce (fn [acc axis]
            (reduce #(assoc % (create-key axis %2) false)
                    acc
                    (range 3)))
          {}
          (repeatedly n #(zero? (rand-int 2)))))

in repl:

user> (launch 5)

{"40" false, "07" false, "19" false, 
 "46" false, "87" false, "47" false, 
 "41" false, "62" false, "86" false}

or (in case you don't like reduce):

(defn launch [n]
  (zipmap (mapcat #(map (partial create-key %) (range 3))
                  (repeatedly n #(zero? (rand-int 2))))
          (repeat false)))

the third variant is to use list comprehension to generate keys:

(defn launch [n]
  (zipmap (for [_ (range n)
                :let [axis (zero? (rand-int 2))]
                i (range 3)]
            (create-key axis i))
          (repeat false)))

all three of them are idiomatic ones, i guess, so it's up to you to choose one, according to your own preferred programming style.

notice that the resulting keys are shuffled inside the map, because unsorted maps don't preserve order. If it is important, you should use sorted-map

What about your variant, the one generating error is this:

(for [k (range 3)] (assoc acc (str y (+ x k)) false))

it doesn't put all the keys to one map, rather it generates a seq of three items equalling (assoc acc k false):

(let [acc {}]
  (for [k (range 3)] (assoc acc k false)))
;;=> ({0 false} {1 false} {2 false})

to do what you want, you use reduce:

(let [acc {}]
  (reduce #(assoc %1 %2 false) acc (range 3)))
;;=> {0 false, 1 false, 2 false}
0
votes

leetwinski has given a more concise answer, but I thought I would post this anyway, since I basically left your structure intact, and this may help you see the error a bit more clearly.

First, I am not sure why you were rebinding acc to the value of an anonymous function call. Your let will happily return a result; so, you should probably do some thinking about why you thought it was necessary to create an anonymous function.

Second, the problem is that for returns a lazy seq, and you are binding this to what you think is a map data structure. This explains why it works fine for cases 0 and 1, but when you use a value of 2 it fails.

Since I don't really fully understand what you're trying to accomplish, here is your original code, modified to work. Disclaimer--this is not really idiomatic and not how I would write it, but I'm posting because it may be helpful to see versus the original, since it actually works.

(defn launch
  [n]
  (loop [cnt n
         acc {}]
    (if (= cnt 0)
      acc
    (recur
      (dec cnt)
      (into acc
        (let [axis (rand-int 2)]
          (if (= axis 0)
            (let [x (rand-int 8) y (rand-int 10)]
              (map #(hash-map (str y (+ x %)) false) (range 3)))
            (let [x (rand-int 10) y (rand-int 8)]
              (map #(hash-map (str (+ y %) x) false) (range 3))))))))))