【问题标题】:conditions in clojure to build a string and update a variableclojure 中构建字符串和更新变量的条件
【发布时间】:2022-01-13 14:52:44
【问题描述】:

我想我会尝试 clojure,做一个打印从开始到目标的路径的拼图。 我的尝试没有打印任何内容。

我知道我已经以程序方式编写了此代码,但我不确定以功能方式编写此代码的最佳方式。所以我想了解为什么这不打印任何内容,如何让条件执行 2 个动作(即:将方向添加到字符串并更新位置。我在网上找到的所有示例都只执行 1 个动作),以及如何真正使这项工作。理想情况下,如何使我的方法发挥作用,以及理想的 clojure 方法是什么。

(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)))))

谢谢。

【问题讨论】:

  • 如果你这样做(while true ...),退出条件在哪里?循环应该中断/结束的条件在哪里?
  • 好点。在我使用它的情况下,拼图引擎会观察何时找到解决方案,但结束条件是 x==t_x && y==t_y
  • 谢谢,我想通了(见我的回答)——翻译你的代码后,我意识到它有一个端点——使用loop-recur 时——但(while true ...) 绝对需要一个条件测试突破..(实际上没有调用任何列出的子句(并且返回 nil 导致 loop-recur 循环停止。我认为 recur 的最后一个解决方案使用 Clojure 的特性最好的。
  • 对于所有为这个问题留下答案的人,我从阅读您的示例中学到的东西比我从几天阅读文档中学到的更多。谢谢你。应该有更多带有此类示例的 clojure 文档。

标签: loops clojure multiple-conditions


【解决方案1】:

此解决方案不使用循环或递归,而是使用sequences,这是 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"

【讨论】:

  • clojure 是如何处理 ns 结尾的未知数 nil 的?
  • Clojure 序列是延迟实现的。这意味着元素是按需从序列中生成的。在我的回答中,nsewnsew 是无限长序列,但 steps 是有限长序列,因此 join 可以正常工作。
  • map 函数(我更喜欢mapv)会将所有输入序列截断为最短序列的长度。此外,str 函数会将nil 转换为空字符串。这让我在尝试编写我的第一个 Cloure 程序时感到非常困惑,而且它不是我最喜欢的技巧!
【解决方案2】:
  1. 首先,您要将变量视为可变变量。 Clojure 中的变量不能像那样修改。除非您将它们声明为atomsatoms 是特殊的可变变量。
  2. (while true ...) 非常不符合 Clojure 风格,非常 C-ish。将(loop [ ... ] ...)recur 结合使用。但为此,您必须了解looprecur 的语法。
  3. 您的(let [m (str m "N")]) let 表单在执行任何操作之前关闭。本地分配[m (str m "N")] 仅在let 表单在] 和最后一个) 之间关闭s​​- 之前有效。你在关闭后做(- y 1)。 以下所有cond 子句都是这种情况。 最后,您执行(println m),因为它在(let [m ""] 表单内,然后将打印""

对于 Clojure 原子,文档是 here 它作为示例列出:

;; 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

然而,作为一名命令式程序员,人们会根据a = new_value 进行思考。最好使用(reset! a new-value-of-a)。因为swap! 一开始也完全误导了我。 假设你想做a = a + 1。然后你必须想好有什么功能: a = func(a)? - 应该是inc。那么a = a + 1 等价于(swap! a inc)。 => 这组的a(inc a)。但是假设你想增加不止 1,比如说增加 3。那么除了a 之外,你还必须给inc 加上额外的参数。假设您希望将a 设置为(inc a 3)。然后这个交换!调用看起来像:(swap! a incf 3)

因此,您必须以这种方式将所有变量(xym)声明为原子。并使用swap!(或者对于初学者更容易的reset! 来更新它们的值。最终,当访问可变变量应该是线程安全的时,您必须使用偶数引用。

loop [variables] &lt;actions&gt; recur)使用递归循环的解决方案

啊,但我知道这不是游戏情况 - 只是一些运行自身的进程。

对于这种情况,我将您的尝试转换为递归循环。

(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 "))))

打印出来:

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

现在我明白了,您的 t_xt_y 是目标坐标。

对于SENE 等这样的组合动作,您必须引入测试它们的子句,例如 (and (&lt; y ty) (&lt; x tx)) (recur (+ x 1) (+ y 1) tx ty (str m "E ")) 和其他此类子句。

如我所见,txty 永远不会改变。所以把它们从loop-recur循环中去掉:

(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 ")))))

打印出来:

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 

【讨论】:

  • 非常感谢您的所有解释。
【解决方案3】:

我的建议是使 [dx dy] 的惰性序列接近尾声。像这样的:

(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])

所以接下来您需要将所有内容转换为人类可读的指示:

(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")

【讨论】:

  • 哇,这太简洁了!而且非常像 Clojure。
  • 我曾想过使用compare,但认为它对于初学者来说可能有点“棘手”。我非常喜欢两阶段解决方案的想法,在阶段 1 计算“增量步骤”,然后在阶段 2 转换为字符串标题。
【解决方案4】:

我会这样写,使用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))))))

以及一些单元测试以显示它的工作原理:

(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"}))

以及最终结果:

(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")))

函数 next-xnext-y 中仍有一些明显的重复可以合并,update-state 可以稍微清理一下,但我想保持简单,无需使用更高级的方式启动功能或辅助功能。


如需参考,请参阅this list of documentation sources,尤其是 Clojure CheatSheet 和《Getting Clojure》一书

关于您的问题:

  1. (&lt; y y-tgt) {:y (inc y) :dir "N"} - Clojure 通常使用“关键字”而不是字符串来命名映射中的字段。在源代码中,它们在前面有一个冒号,而不是一对引号。

  2. pos-next {:x (:x x-info) :y (:y y-info)} - 正确。返回值是一个带有键:x:y的新映射,它们是从变量x-infoy-info中的映射复制而来的

  3. loop 语句视为类似于let 块。每对的第一个符号定义一个新的“循环变量”,每对的第二个符号是该变量的初始值。 由于只有 1 对,因此只有 1 个循环变量,因此 recur 语句中只有 1 个值。 loop/recur 表单可以有零个或多个循环变量。

【讨论】:

  • (
  • pos-next {:x (:x x-info) :y (:y y-info)} - :y (:y y-info) 是说使用 y 中的 :y 值-info 并将其分配给 pos-next 中的 :y 值?
  • 感谢您抽出宝贵时间撰写本文。我花了一些时间在精神上解析这一点。使用 (recur (:pos-next state-next) 它有 1 个参数。这是否知道要在循环 [pos pos-init] 中更新 pos,因为它是第一个参数?(我见过一些示例,它们给出了 2 个参数重复)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-20
  • 1970-01-01
  • 1970-01-01
  • 2011-06-25
  • 1970-01-01
  • 2019-12-21
  • 1970-01-01
相关资源
最近更新 更多