I thought I'd tryout clojure, doing a puzzle printing a path from start to target. My attempt is printing nothing.
I understand i've written this in a procedural way, but am not sure the best way to thinking about writing this functionally. So I'd like to understand why this prints nothing, how to have a condition execute 2 actions (ie: add the direction to the string and update the position. All the examples I found online only execute 1 action), as well as how to actually make this work. Ideally, how to make my approach work, and also what would be the ideal clojure approach.
(defn -main [& args]
(def x 2)
(def y 3)
(def t_x 10)
(def t_y 15)
(while true
(let [m ""]
(cond
(> y t_y) (let [m (str m "N")])(- y 1)
(< y t_y) (let [m (str m "S")])( y 1)
(> x t_x) (let [m (str m "W")])(- x 1)
(< x t_x) (let [m (str m "E")])( x 1))
; A single line providing the move to be made: N NE E SE S SW W or NW
(println m)))))
Thank you.
CodePudding user response:
Rather than use loops or recursion, this solution uses sequences, a prevalent abstraction in Clojure:
(defn axis-steps [a b axis axis']
(concat
(cond
(< a b) (repeat (- b a) axis)
(< b a) (repeat (- a b) axis'))
(repeat nil)))
(defn path [x y tx ty]
(let [ns (axis-steps y ty "S" "N") ; = ("N" "N" nil nil nil ...)
ew (axis-steps x tx "E" "W") ; = ("E" "E" "E" nil nil nil ...)
nsew (map str ns ew) ; = ("NE" "NE" "E" "" "" "" ... )
steps (take-while seq nsew)] ; = ("NE" "NE" "E")
(clojure.string/join " " steps))) ; = "NE NE E"
(path 2 3 10 15) ; => "SE SE SE SE SE SE SE SE S S S S"
CodePudding user response:
- First of all, you want to treat the variables like mutable variables. Variables in Clojure are not modifiable like that. Except you declare them as
atoms
.atom
s are special mutable variables. (while true ...)
is very un-clojure-ish and veryC-ish
. Use(loop [ ... ] ...)
in combination withrecur
. But for that you have to understand the syntax ofloop
andrecur
.- Your
(let [m (str m "N")])
the let form closes before doing anything. The local assignment[m (str m "N")]
is only valid until thelet
form closes s- between the]
and the last)
. You do(- y 1)
after the closed let. And this is the case in all followingcond
clauses. Finally you do(println m)
which, since it is within the(let [m ""]
form, would then print the""
.
To Clojure atoms, the documentation is here it lists as example:
;; a var to be used for its side effects
(def a (atom 10))
;; #'user/a
(while (pos? @a)
(println @a)
(swap! a dec))
;; 10
;; 9
;; 8
;; 7
;; 6
;; 5
;; 4
;; 3
;; 2
;; 1
;;=> nil
However, as an imperative programmer, one thinks in terms of a = new_value
. where it is better then to use (reset! a new-value-of-a)
. Because swap!
misled me also totally at the beginning.
Let's say you want to do a = a 1
. Then you have to think okay what function is there which does:
a = func(a)
? - it would be inc
. then a = a 1
is equivalent to (swap! a inc)
. => this set's a
to (inc a)
. But let's say youw ant to increase by more than just 1, let's say by 3. Then you hav eto give inc
also the addiional argument in addition to a
. Let's say you want a
to be set to (inc a 3)
. Then this swap! call would look like: (swap! a incf 3)
.
So you have to declare all your variables (x
, y
, m
) as atoms in this way. And use either swap!
(or for the beginner easier reset!
to update their values. Eventually, you have to use even references, when access to the mutable variable should be thread-safe.
Solution using recursive loop by loop [variables] <actions> recur)
Ah but I see this is not a games situation - but just some process running itself.
For this case, I translated your attempt into a recursive loop.
(loop [x 2
y 3
tx 10
ty 15
m ""]
(println m)
(cond (< ty y) (recur x (- y 1) tx ty (str m "N "))
(< y ty) (recur x ( y 1) tx ty (str m "S "))
(< tx x) (recur (- x 1) y tx ty (str m "W "))
(< x tx) (recur ( x 1) y tx ty (str m "E "))))
It prints:
S
S S
S S S
S S S S
S S S S S
S S S S S S
S S S S S S S
S S S S S S S S
S S S S S S S S S
S S S S S S S S S S
S S S S S S S S S S S
S S S S S S S S S S S S
S S S S S S S S S S S S E
S S S S S S S S S S S S E E
S S S S S S S S S S S S E E E
S S S S S S S S S S S S E E E E
S S S S S S S S S S S S E E E E E
S S S S S S S S S S S S E E E E E E
S S S S S S S S S S S S E E E E E E E
S S S S S S S S S S S S E E E E E E E E
;; => nil
Now I see, your t_x
and t_y
were the target coordinates.
for SE
, NE
etc. such combined movements, you have to introduce clauses which tests for them e.g.
(and (< y ty) (< x tx)) (recur ( x 1) ( y 1) tx ty (str m "E "))
and other such clauses.
As I see, tx
and ty
are not changing ever. So put them out of the loop
-recur
loop:
(let [tx 10 ty 15]
(loop [x 2
y 3
m ""]
(when (not= m "") ; print only when m is not an empty string
(println m))
(cond (and (< ty y) (< x tx)) (recur ( x 1) (- y 1) (str m "NE "))
(and (< y ty) (< x tx)) (recur ( x 1) ( y 1) (str m "SE "))
(and (< ty y) (< tx x)) (recur (- x 1) (- y 1) (str m "NW "))
(and (< y ty) (< x tx)) (recur (- x 1) ( y 1) (str m "SW "))
(< ty y) (recur x (- y 1) (str m "N "))
(< y ty) (recur x ( y 1) (str m "S "))
(< tx x) (recur (- x 1) y (str m "W "))
(< x tx) (recur ( x 1) y (str m "E ")))))
It prints:
SE
SE SE
SE SE SE
SE SE SE SE
SE SE SE SE SE
SE SE SE SE SE SE
SE SE SE SE SE SE SE
SE SE SE SE SE SE SE SE
SE SE SE SE SE SE SE SE S
SE SE SE SE SE SE SE SE S S
SE SE SE SE SE SE SE SE S S S
SE SE SE SE SE SE SE SE S S S S
CodePudding user response:
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.