Here is how I would write it, using my favorite template project & library:
(ns demo.core
(:use tupelo.core))
(defn next-x
[x x-tgt]
(cond
(< x x-tgt) {:x (inc x) :dir "E"}
(> x x-tgt) {:x (dec x) :dir "W"}
:else {:x x :dir ""}))
(defn next-y
[y y-tgt]
(cond
(< y y-tgt) {:y (inc y) :dir "N"}
(> y y-tgt) {:y (dec y) :dir "S"}
:else {:y y :dir ""}))
(defn update-state
[pos pos-goal]
(let [x-info (next-x (:x pos) (:x pos-goal))
y-info (next-y (:y pos) (:y pos-goal))
pos-next {:x (:x x-info) :y (:y y-info)}
dir-str (str (:dir y-info) (:dir x-info))
state-next {:pos-next pos-next :dir-str dir-str}]
state-next))
(defn walk-path [pos-init pos-goal]
(loop [pos pos-init]
(when (not= pos pos-goal)
(let [state-next (update-state pos pos-goal)]
(println (:dir-str state-next))
(recur (:pos-next state-next))))))
and some unit tests to show it working:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(dotest
(is= (next-x 0 5) {:x 1, :dir "E"})
(is= (next-x 6 5) {:x 5, :dir "W"})
(is= (next-x 5 5) {:x 5, :dir ""})
(is= (next-y 0 5) {:y 1, :dir "N"})
(is= (next-y 6 5) {:y 5, :dir "S"})
(is= (next-y 5 5) {:y 5, :dir ""}))
(dotest
(is= (update-state {:x 0, :y 0} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "NE"})
(is= (update-state {:x 1, :y 0} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "N"})
(is= (update-state {:x 2, :y 0} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "NW"})
(is= (update-state {:x 0, :y 1} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "E"})
(is= (update-state {:x 1, :y 1} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str ""})
(is= (update-state {:x 2, :y 1} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "W"})
(is= (update-state {:x 0, :y 2} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "SE"})
(is= (update-state {:x 1, :y 2} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "S"})
(is= (update-state {:x 2, :y 2} {:x 1, :y 1}) {:pos-next {:x 1, :y 1}, :dir-str "SW"}))
and the final result:
(dotest
(let [pos-init {:x 0 :y 0}
pos-goal {:x 3 :y 5}
str-result (with-out-str
(walk-path pos-init pos-goal))]
; (println str-result) ; uncomment to print result
(is-nonblank= str-result
"NE
NE
NE
N
N")))
There is still some obvious duplication in the functions next-x & next-y that could be consolidated, and update-state could be cleaned up a little, but I wanted to keep it simple to start w/o using more advanced features or helper functions.
For reference, please see this list of documentation sources, especially the Clojure CheatSheet and the book "Getting Clojure"
Regarding your questions:
(< y y-tgt) {:y (inc y) :dir "N"} -
Clojure usually uses "keywords" instead of strings to name the fields in a map. In source code, these have a single colon at the front, instead of a pair of quotes.
pos-next {:x (:x x-info) :y (:y y-info)} - Correct. The return value is a new map with keys :x and :y, which are copied from the maps in the variables x-info and y-info
Think of a loop statement as similar to a let block. The first symbol of each pair defines a new "loop variable", and the 2nd symbol of each pair is the initial value of that variable.
Since there is only 1 pair, there is only 1 loop variable and hence only 1 value in the recur statement.
A loop/recur form can have zero or more loop variables.
(while true ...)where is the exit condition? where is the condition when the loop should break/end?loop-recur- but a(while true ...)needs definitely a condition to test for break out.. (it is when actually none of the listed clauses are called (and nil is returned which brings theloop-recurloop to stop. I think the last solution withrecuruses Clojure's peculiarities the best.