简体   繁体   中英

clojure - update map values in different ways based on key value

I want to represent a graph of 2D positions + neighbors in Clojure for a board game. I am using a map that maps a position to a vector of neighbors:

{[0 0] [[0 1] [1 0] [1 1]]}

I have written some functions that can generate the neighbor graph for any sized board:

(defn positions [size]
  (for [x (range 0 size) y (range 0 size)] [x y]))

(defn neighbors [size [x y]]
  (filter (fn [[x y]]
        (and (>= x 0) (< x size) (>= y 0) (< y size)))
      (-> []
          (conj [(inc x) y])
          (conj [(dec x) y])
          (conj [x (inc y)])
          (conj [x (dec y)])
          (conj [(inc x) (inc y)])
          (conj [(dec x) (dec x)]))))

(defn board-graph
  [size]
  (reduce (fn [map position] (assoc map
                                    position
                                    (neighbors size position)))
          {}
          (positions size)))

This works fine:

(board-graph 2)
=> {[0 0] ([1 0] [0 1] [1 1]), [0 1] ([1 1] [0 0]), [1 0] ([0 0] [1 1] [0 0]), [1 1] ([0 1] [1 0] [0 0])}

However I now want to add to this additional 'virtual neighbors' to each of the board positions that are on the edge of the board, .eg :TOP, :BOTTOM, :LEFT, :RIGHT. So I'd like:

(board-graph 2)
=> {[0 0] (:LEFT :TOP [1 0] [0 1] [1 1]), [0 1] (:LEFT :BOTTOM [1 1] [0 0]), [1 0] (:RIGHT :TOP [0 0] [1 1] [0 0]), [1 1] (:RIGHT :BOTTOM [0 1] [1 0] [0 0])}

Here is my attempt so far, but it doesn't quite work right and it seems really over-complicated:

(defn- filter-keys
  [pred map]
  (into {}
        (filter (fn [[k v]] (pred k)) map)))


(defn board-graph
  [size]
  (let [g (reduce (fn [map position] (assoc map
                                            position
                                            (neighbors size position)))
                  {}
                  (positions size))]
    (merge g
           (reduce-kv #(assoc %1 %2 (conj %3 :TOP)) {}
                      (filter-keys (fn [[x y]] (= y 0)) g))
           (reduce-kv #(assoc %1 %2 (conj %3 :BOTTOM)) {}
                      (filter-keys (fn [[x y]] (= y (dec size))) g))
           (reduce-kv #(assoc %1 %2 (conj %3 :LEFT)) {}
                      (filter-keys (fn [[x y]] (= x 0)) g))
           (reduce-kv #(assoc %1 %2 (conj %3 :RIGHT)) {}
                      (filter-keys (fn [[x y]] (= x (dec size))) g)))))

I basically want to build my map, go over it again and for certain keys update the value associated there depending on what the key is. I can't find a nice way to do this without resorting to state! Is there a more idiomatic way of doing this?

You can just add a few more expressions to your threaded expression where you build the returned vector. To make this read more nicely I switched it from the thread first macro -> to the "thread as" macro as-> which binds the symbol (in this case) c to the value being threaded at each step so I can use it in an a conditional expression on some of the stages:

(defn neighbors [size [x y]]
  (filter (fn [[x y]]
            (and (>= x 0) (< x size) (>= y 0) (< y size)))
          (as-> [] c
              (conj c [(inc x) y])
              (conj c [(dec x) y])
              (conj c [x (inc y)])
              (conj c [x (dec y)])
              (conj c [(inc x) (inc y)])
              (conj c [(dec x) (dec x)])
              (if (zero? x)  (conj c :TOP) c)
              (if (= size x) (conj c :BOTTOM) c)
              (if (zero? y)  (conj c :LEFT) c)
              (if (= size y) (conj c :RIGHT) c))))

each conditional expression either returns an updated version, or passes it through unchanged if the condition was not met.

This works for me, but it isn't very close to what you wrote originally.

(defn positions [size]
  (for [x (range 0 size) y (range 0 size)] [x y]))

(defn neighbors [n [x y]]
  (let [left (if (zero? x)
               :LEFT
               [(dec x) y])
        right (if (= x (dec n))
                :RIGHT
                [(inc x) y])
        up (if (zero? y)
             :TOP
             [x (dec y)])
        down (if (= y (dec n))
               :BOTTOM
               [x (inc y)])]
    (list left right up down)))

(defn board-graph [n]
  (let [keys (positions n)
        vals (map (partial neighbors n) keys)]
    (zipmap keys vals)))

then

(clojure-scratch.core/board-graph 2)
=>
{[1 1] ([0 1] :RIGHT [1 0] :BOTTOM),
 [1 0] ([0 0] :RIGHT :TOP [1 1]),
 [0 1] (:LEFT [1 1] [0 0] :BOTTOM),
 [0 0] (:LEFT [1 0] :TOP [0 1])}

EDIT: As Arthur noted, this doesn't handle diagonals, if you want that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM