【问题标题】:Idiomatic way to run nested loop with passing value使用传递值运行嵌套循环的惯用方式
【发布时间】:2021-02-16 05:23:16
【问题描述】:

我想做这样的事情

int n=0
for(int i=xs; i<xe; i++){
  for(int j=ys; j<ye; j++){
    n++
  }
}
return n;

以 Clojure 方式。由于所有值都是不可变的,我认为值 n 应该作为(可能)递归函数的参数传递。最好的方法是什么?

【问题讨论】:

  • 这不只是返回(* (- xe xs) (- ye ys)) 吗?
  • 如果你打算在内部循环体中做一些比n++ 更有趣的事情,答案可能取决于那个东西是否有副作用,比如打印,或者这个想法是否是构建某种新的集合,或者转换现有的集合等。在大多数情况下,您可以通过使用高阶函数来避免递归。
  • @jas 是的,这就是这个例子。实际上,n++ 操作是不同的,要复杂得多。关键是如何在嵌套递归中将变量 n 交给参数。
  • 仅举一个例子,请参阅clojuredocs.org/clojure.core/map-indexed,但如果不了解您要做什么,就很难说更多。

标签: loops clojure nested-loops idioms code-translation


【解决方案1】:

想到宏。我定义了一个宏for-state,我像这样使用它:

(def xs 0)
(def xe 9)
(def ys 1)
(def ye 4)

(for-state
 i xs (< i xe) (inc i) n 0
 (for-state
  j ys (< j ye) (inc j) n n
  (inc n)))
;; => 27

宏允许您添加仅使用函数难以构建的新结构。因此,如果您有多个此类嵌套循环,则可以选择定义像 for-state 这样的宏:

(defmacro for-state [iter-var iter-init iter? iter-next    
                     state-var state-init state-next]
  `(loop [~iter-var ~iter-init
          ~state-var ~state-init]
     (if ~iter?
       (recur ~iter-next
              ~state-next)
       ~state-var)))

您可以随意调整它。例如,您可以使用向量对宏参数进行分组,并对这些参数进行解构以提高可读性。

【讨论】:

    【解决方案2】:

    我认为在这里您可以将reduce 函数应用于for。你在loop-processing-fn 中做什么取决于你——它也可以是递归的。

    (let [n-init 0 ;; your `n` variable
          xs 10 xe 20 ys -5 ye 5 ;; loop(s) ranges
          loop-processing-fn (fn [current-state [i j :as loop-data]]
                               (inc current-state) ;; anything here 
                               ) ;; processing function operating on state (n) and loop data
    
          ]
      (reduce loop-processing-fn n-init (for [i (range xs xe)
                                              j (range ys ye)]
                                          [i j])))
    ;; => 100
    

    【讨论】:

      【解决方案3】:

      最接近您的代码的是

      (defn f [xs xe ys ye]
        (let [n (atom 0)]
          (doseq [_ (range xs xe)
                  _ (range ys ye)]
            (swap! n inc))
          @n))
      
      user> (f 1 10 2 20)
      ;;=> 162
      

      但是可变原子方法根本是单一的。

      它可能看起来像这样,多一点 clojure 方式

      (defn f [xs xe ys ye]
        (count (for [_ (range xs xe)
                     _ (range ys ye)]
                 nil)))
      #'user/f
      
      user> (f 1 10 2 20)
      ;;=> 162
      

      这真的取决于你想要做什么。 @jas 注意到,(* (- xe xs) (- ye ys)) 显然可以更好地计算 n,这与您使用的语言无关)

      你提到的递归解决方案怎么样,它可能看起来像这样:

      (defn f [xs xe ys ye]
        (loop [n 0 i xs j ys]
          (cond (== j ye) n
                (== i xe) (recur n xs (inc j))
                :else (recur (inc n) (inc i) j))))
      #'user/f
      
      user> (f 1 10 2 20)
      ;;=> 162
      

      【讨论】:

      • 我认为最后的循环示例最接近问题的精神,并且在 Clojure 中是一件非常自然的事情。
      【解决方案4】:

      不要过度思考问题。当你真的需要可变状态时,你总是可以使用atom

      (defn calc
        [xs ys]
        (let [result (atom 0)]
          (doseq [x xs]
            (doseq [y ys]
              (swap! result + (* x y))))
          @result))
      
        (let [xs     [1 2 3]
              ys     [2 5 7 9]]
          (calc xs ys))
      

      结果

      (calc xs ys) => 138
      

      您也可以使用volatile。它就像一个非线程安全的原子。注意vswap!的使用:

      (defn calc
        [xs ys]
        (let [result (volatile! 0)]
          (doseq [x xs]
            (doseq [y ys]
              (vswap! result + (* x y))))
          @result))
      

      性能

      在一个紧密的循环中,使用volatile 会有所作为。一个例子:

      (ns tst.demo.core
        (:use tupelo.core tupelo.test)
        (:require [tupelo.profile :as prof]))
      
      (def N 100)
      (def vals (vec (range N)))
      
      (prof/defnp summer-atom []
        (let [result (atom 0)]
          (doseq [i vals]
            (doseq [j vals]
              (doseq [k vals]
                (swap! result + i j k))))
          @result))
      
      (prof/defnp summer-volatile []
        (let [result (volatile! 0)]
          (doseq [i vals]
            (doseq [j vals]
              (doseq [k vals]
                (vswap! result + i j k))))
          @result))
      
      (dotest
        (prof/timer-stats-reset)
        (dotimes [i 10]
          (spyx (summer-atom))
          (spyx (summer-volatile)))
        (prof/print-profile-stats))
      

      结果:

      --------------------------------------
         Clojure 1.10.2-alpha1    Java 15
      --------------------------------------
      
      Testing tst.demo.core
      
      (summer-atom)     => 148500000
      (summer-volatile) => 148500000
      ...
      
      ---------------------------------------------------------------------------------------------------
      Profile Stats:
         Samples       TOTAL        MEAN      SIGMA           ID
             10        2.739     0.273879   0.023240   :tst.demo.core/summer-atom                                                       
             10        0.383     0.038313   0.041246   :tst.demo.core/summer-volatile                                                   
      ---------------------------------------------------------------------------------------------------
      

      所以它产生了大约 10 倍的差异。除非您像这里 (100^3) 那样进行至少一百万次操作,否则可能不值得。

      关于数据结构的类似低级操作,请看transient!和朋友。

      特别是书签 Clojure CheatSheet from this list

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多