简体   繁体   English

我可以在 Clojure 中的函数组合实现中使用“recur”吗?

[英]Can I use `recur` in this implementation of function composition in Clojure?

Consider this simple-minded recursive implementation of comp in Clojure:考虑一下 Clojure 中这个简单的comp递归实现:

(defn my-comp
  ([f]
   (fn [& args]
     (apply f args)))
  ([f & funcs]
   (fn [& args]
     (f (apply (apply my-comp funcs) args)))))

The right way to do this, I am told, is using recur , but I am unsure how recur works.有人告诉我,正确的方法是使用recur ,但我不确定recur是如何工作的。 In particular: is there a way to coax the code above into being recur able?特别是:有没有办法让上面的代码可以recur

a way to do this, is to rearrange this code to pass some intermediate function back up to the definition with recur.一种方法是重新排列此代码以将一些中间函数传递回使用 recur 的定义。

the model would be something like this:该模型将是这样的:

(my-comp #(* 10 %) - +)

(my-comp (fn [& args] (#(* 10 %) (apply - args)))
           +)

(my-comp (fn [& args]
             ((fn [& args] (#(* 10 %) (apply - args)))
              (apply + args))))

the last my-comp would use the first my-comp overload (which is (my-comp [f])最后一个 my-comp 将使用第一个 my-comp 重载(即(my-comp [f])

here's how it could look like:这是它的样子:

(defn my-comp
  ([f] f)
  ([f & funcs]
   (if (seq funcs)
     (recur (fn [& args]
              (f (apply (first funcs) args)))
            (rest funcs))
     (my-comp f))))

notice that despite of not being the possible apply target, the recur form can still accept variadic params being passed as a sequence.请注意,尽管不是可能的apply目标,但recur表单仍然可以接受作为序列传递的可变参数。

user> ((my-comp (partial repeat 3) #(* 10 %) - +) 1 2 3)
;;=> (-60 -60 -60)

notice, though, that in practice this implementation isn't really better than yours: while recur saves you from stack overflow on function creation, it would still overflow on application (somebody, correct me if i'm wrong):但是请注意,实际上这个实现并不比你的更好:虽然recur可以让你在函数创建时避免堆栈溢出,但它仍然会在应用程序上溢出(如果我错了,请纠正我):

(apply my-comp (repeat 1000000 inc)) ;; ok

((apply my-comp (repeat 1000000 inc)) 1) ;; stack overflow

so it would probably be better to use reduce or something else:所以使用reduce或其他东西可能会更好:

(defn my-comp-reduce [f & fs]
  (let [[f & fs] (reverse (cons f fs))]
    (fn [& args]
      (reduce (fn [acc curr-f] (curr-f acc))
               (apply f args)
               fs))))

user> ((my-comp-reduce (partial repeat 3) #(* 10 %) - +) 1 2 3)
;;=> (-60 -60 -60)

user> ((apply my-comp-reduce (repeat 1000000 inc)) 1)
;;=> 1000001

There is already a good answer above, but I think the original suggestion to use recur may have been thinking of a more manual accumulation of the result.上面已经有了很好的答案,但我认为使用recur的最初建议可能是在考虑对结果进行更手动的累积。 In case you haven't seen it, reduce is just a very specific usage of loop/recur :如果您还没有看到它, reduce只是loop/recur的一个非常具体的用法:

(ns tst.demo.core
  (:use demo.core tupelo.core  tupelo.test))

(defn my-reduce
  [step-fn init-val data-vec]
  (loop [accum init-val
         data  data-vec]
    (if (empty? data)
      accum
      (let [accum-next (step-fn accum (first data))
            data-next  (rest data)]
        (recur accum-next data-next)))))

(dotest
  (is=  10 (my-reduce + 0 (range 5)))    ; 0..4
  (is= 120 (my-reduce * 1 (range 1 6)))  ; 1..5 )

In general, there can be any number of loop variables (not just 2 like for reduce).一般来说,可以有任意数量的循环变量(而不仅仅是 2 像 reduce)。 Using loop/recur gives you a more "functional" way of looping with accumulated state instead of using and atom and a doseq or something.使用loop/recur为您提供了一种更“功能性”的循环累积状态的方式,而不是使用 and atomdoseq或其他东西。 As the name suggests, from the outside the effect is quite similar to a normal recursion w/o any stack size limits (ie tail-call optimization).顾名思义,从外部看,效果非常类似于没有任何堆栈大小限制的正常递归(即尾调用优化)。


PS As this example shows, I like to use a let form to very explicitly name the values being generated for the next iteration. PS如本例所示,我喜欢使用let形式来非常明确地命名为下一次迭代生成的值。

PPS While the compiler will allow you to type the following w/o confusion: PPS虽然编译器将允许您键入以下内容而不会混淆:

(ns tst.demo.core
  (:use demo.core tupelo.core  tupelo.test))

(defn my-reduce
  [step-fn accum data]
  (loop [accum accum
         data  data]
     ...))

it can be a bit confusing and/or sloppy to re-use variable names (esp. for people new to Clojure or your particular program).重用变量名可能有点混乱和/或草率(尤其是对于 Clojure 或您的特定程序的新手)。


Also

I would be remiss if I didn't point out that the function definition itself can be a recur target (ie you don't need to use loop ).如果我没有指出函数定义本身可以是recur目标(即您不需要使用loop ),那我就失职了。 Consider this version of the factorial:考虑这个版本的阶乘:

(ns tst.demo.core
  (:use demo.core tupelo.core  tupelo.test))

(defn fact-impl
  [cum x]
  (if (= x 1)
    cum
    (let [cum-next (* cum x)
          x-next   (dec x)]
      (recur cum-next x-next))))

(defn fact [x] (fact-impl 1 x))

(dotest
  (is= 6 (fact 3))
  (is= 120 (fact 5)))

evaluation 1评价1

First let's visualize the problem.首先让我们可视化问题。 my-comp as it is written in the question will create a deep stack of function calls, each waiting on the stack to resolve, blocked until the the deepest call returns -问题中写的my-comp将创建一个很深的函数调用堆栈,每个函数调用都在堆栈上等待解决,阻塞直到最深的调用返回 -

((my-comp inc inc inc) 1)
((fn [& args]
     (inc (apply (apply my-comp '(inc inc)) args))) 1)

(inc (apply (fn [& args]
                (inc (apply (apply my-comp '(inc)) args))) '(1)))

(inc (inc (apply (apply my-comp '(inc)) '(1))))

(inc (inc (apply (fn [& args]
                     (apply inc args)) '(1))))

(inc (inc (apply inc '(1)))) ; ⚠️ deep in the hole we go...
(inc (inc 2))
(inc 3)
4

tail-recursive my-comp尾递归 my-comp

Rather than creating a long sequence of functions, this my-comp is refactored to return a single function, which when called, runs a loop over the supplied input functions -与其创建一长串函数,不如将此my-comp重构为返回一个函数,该函数在调用时会在提供的输入函数上运行一个loop -

(defn my-comp [& fs]
  (fn [init]
    (loop [acc init [f & more] fs]
      (if (nil? f)
          acc
          (recur (f acc) more))))) ; 🐍 tail recursion
((my-comp inc inc inc) 1)
;; 4
((apply my-comp (repeat 1000000 inc)) 1)
;; 1000001

evaluation 2评价2

With my-comp rewritten to use loop and recur , we can see linear iterative evaluation of the composition -my-comp重写为使用looprecur ,我们可以看到组合的线性迭代评估 -

((my-comp inc inc inc) 1)
(loop 1 (list inc inc inc))
(loop 2 (list inc inc))
(loop 3 (list inc))
(loop 4 nil)
4

multiple input args多个输入参数

Did you notice ten (10) apply calls at the beginning of this post?您是否注意到本文开头有十 (10) 个apply电话? This is all in service to support multiple arguments for the first function in the my-comp sequence.这一切都是为了支持my-comp序列中第一个函数的多个参数。 It is a mistake to tangle this complexity with my-comp itself.将这种复杂性与my-comp本身纠缠在一起是错误的。 The caller has control to do this if it is the desired behavior.如果这是所需的行为,调用者可以控制执行此操作。

Without any additional changes to the refactored my-comp -无需对重构my-comp进行任何额外更改 -

((my-comp #(apply * %) inc inc inc) '(3 4)) ; ✅ multiple input args

Which evaluates as -评估为 -

(loop '(3 4) (list #(apply * %) inc inc inc))
(loop 12 (list inc inc inc))
(loop 13 (list inc inc))
(loop 14 (list inc))
(loop 15 nil)
15

right-to-left order从右到左的顺序

Above (my-comp abc) will apply a first, then b , and finally c .上面(my-comp abc)将首先应用a ,然后是b ,最后是c If you want to reverse that order, a naive solution would be to call reverse at the loop call site -如果您想反转该顺序,一个天真的解决方案是在loop调用站点调用reverse -

(defn my-comp [& fs]
  (fn [init]
    (loop [acc init [f & more] (reverse fs)] ; ⚠️ naive
      (if (nil? f)
          acc
          (recur (f acc) more)))))

Each time the returned function is called, (reverse fs) will be recomputed.每次调用返回的函数时,都会重新计算(reverse fs) To avoid this, use a let binding to compute the reversal just once -为避免这种情况,请使用let绑定只计算一次反转 -

(defn my-comp [& fs]
  (let [fs (reverse fs)] ; ✅ reverse once
    (fn [init]
      (loop [acc init [f & more] fs]
        (if (nil? f)
            acc
            (recur (f acc) more))))))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM