[英]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 atom
和doseq
或其他东西。 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 或您的特定程序的新手)。
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
重写为使用loop
和recur
,我们可以看到组合的线性迭代评估 -
((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.