Ryan Asensio
Ryan Asensio

Reputation: 87

Updating an atom / scope

I'm trying to change the value of an atom which holds information about the state of a game.

The code is as follows:

(def initial-state {:pos [0 0] :dir go-right})
(defonce app-state (atom initial-state))

(defn go-right [[x y]] [(inc x) y])

(defn new-pos [state]  ((:dir @state) (:pos @state))))

(defn update-state [app-state] 
  (assoc @app-state :pos (new-pos app-state)))

I have a function that should update the :pos of the atom, based on the function stored in ":dir".

My problem is that in the new-pos function, I get an error saying that basically, @state is nil.

Error:

Uncaught TypeError: Cannot read property 'call' of null

What a I missing?

Upvotes: 2

Views: 494

Answers (1)

Dan Prince
Dan Prince

Reputation: 29989

You are defining your atom before you have declared the go-right function. When you dereference it, you'll get nil.

(def app-state (atom { :dir go-right :pos [0 0] }))
(:dir @state) ;; ===> nil

You could re-arrange your code, but I think a better solution would be to use a simpler data type, such as a semantically appropriate keyword.

(def app-state (atom { :dir :right :pos [0 0] }))

Then use it to reference the appropriate function once the atom is dereferenced.

(def movement { :right go-right
                :left  go-left
                :up    go-up
                :down  go-down })

(defn new-pos [state]
  (let [dir  (:dir state)
        pos  (:pos state)
        move (get movement dir)]
    (move pos)))

This way you'll be able to serialize your app-state atom, which will allow you to save the state to disk and load it again later.

I'd also opt for using a generic movement function, rather than hardcoding for each direction.

(defn move [[dx dy] [x y]]
  [(+ dx x) (+ dy y)])

(def movement { :left  (partial move [-1 0])
                :right (partial move [1 0])
                :up    (partial move [0 1])
                :down  (partial move [0 -1]) })

You probably don't want start calling assoc on the manually deferenced atom either. This will result in a need to use reset and you can avoid all of that by using swap in the first place.

We can remove all of the deref calls and let swap handle them instead. Functions are generally more useful if they just work with plain data. Let the lower level dereferencing happen elsewhere.

(defn update-state [app-state] 
  (assoc app-state :pos (new-pos app-state)))

Finally, to update the state atom, use swap.

(swap! app-state update-state)
;; ===> {:dir :right, :pos [1 0]}

Complete Code - Works at Clojurescript.net's REPL

(defn move [[dx dy] [x y]]
  [(+ dx x) (+ dy y)])

(def movement { :left  (partial move [-1 0])
                :right (partial move [1 0])
                :up    (partial move [0 1])
                :down  (partial move [0 -1]) })

(defn new-pos [state]
  (let [dir  (:dir state)
        pos  (:pos state)
        move (get movement dir)]
    (move pos)))

(defn update-state [app-state] 
  (assoc app-state :pos (new-pos app-state)))

(def app-state (atom { :dir go-right :pos [0 0] }))

(swap! app-state update-state)

Upvotes: 4

Related Questions