1

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.

4
  • 1
    If you do (while true ...) where is the exit condition? where is the condition when the loop should break/end? Commented Dec 8, 2021 at 17:42
  • Good point. In the situation I was using it, the puzzle engine watched to see when the solution was found, but the end condition would be x==t_x && y==t_y Commented Dec 8, 2021 at 23:05
  • 1
    Thanks I figured that out (see my answer) - after translating your code I realized it has an endpoint - when using 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 the loop-recur loop to stop. I think the last solution with recur uses Clojure's peculiarities the best. Commented Dec 9, 2021 at 0:06
  • To all of you who left answers to this question, I learned more from reading your examples than I did from reading the docs for days. Thank you. There should be more clojure docs with examples like this. Commented Dec 14, 2021 at 23:56

4 Answers 4

5

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"
Sign up to request clarification or add additional context in comments.

3 Comments

how does clojure deal with the unknown number of nil at the end of ns?
Clojure sequences are lazily realized. This means that elements are produced from the sequence on demand. In my answer, ns, ew and nsew are infinitely long sequences, but steps is a finitely long sequence, so the join works as expected.
The map function (I prefer mapv) will truncate all input sequences to the length of the shortest one. Also, the str function will translate a nil to the empty string. This confused me greatly trying to write my 1st Cloure program, and it is not my favorite trick by a longshot!.
3
  1. 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. atoms are special mutable variables.
  2. (while true ...) is very un-clojure-ish and very C-ish. Use (loop [ ... ] ...) in combination with recur. But for that you have to understand the syntax of loop and recur.
  3. Your (let [m (str m "N")]) the let form closes before doing anything. The local assignment [m (str m "N")] is only valid until the let form closes s- between the ] and the last ). You do (- y 1) after the closed let. And this is the case in all following cond 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 

1 Comment

Thank you so much for all the explanations.
3

what i would propose, is to make the lazy sequence of [dx dy] steps towards the end. Something like this:

(defn path [curr end]
  (when-not (= curr end)
    (lazy-seq
     (let [delta (mapv compare end curr)]
       (cons delta (path (mapv + delta curr) end))))))

user> (path [2 3] [10 15])
;;=> ([1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [1 1] [0 1] [0 1] [0 1] [0 1])

so the next thing you need is to translate everything to human-readable directions:

(defn translate [[dx dy]]
  (str ({-1 \S 1 \N} dx)
       ({-1 \W 1 \E} dy)))

user> (map translate (path [2 3] [10 15]))
;;=> ("NE" "NE" "NE" "NE" "NE" "NE" "NE" "NE" "E" "E" "E" "E")

user> (map translate (path [10 15] [3 21]))
;;=> ("SE" "SE" "SE" "SE" "SE" "SE" "S")

user> (map translate (path [10 15] [3 3]))
;;=> ("SW" "SW" "SW" "SW" "SW" "SW" "SW" "W" "W" "W" "W" "W")

2 Comments

Wow this is very terse! And very Clojure-ish.
I thought of using compare, but thought it might be a bit too "tricky" to spring on a beginner. I do very much like the idea of a two stage solution, calculating the "delta steps" in stage 1, then translating into string headings in stage 2.
2

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:

  1. (< 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.

  2. 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

  3. 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.

3 Comments

(< y y-tgt) {:y (inc y) :dir "N"} - :y and :dir are creating property names in the returned object? Are the colons regular clojure, or the template project?
pos-next {:x (:x x-info) :y (:y y-info)} - :y (:y y-info) is saying use the :y value from y-info and assign it to the :y value in pos-next?
Thank you for taking the time to write this. It took me some time to mentally parse this. With (recur (:pos-next state-next) it has 1 argument. Does this know to update pos in loop[pos pos-init] because it is the first argument? (I've seen some examples where they give 2 arguments to recur)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.