迭代地将函数应用于其结果而不生成seq

时间:2014-04-20 05:23:27

标签: clojure

这是其中之一"是否有内置/更好/惯用/聪明的方法来做到这一点?"的问题。

我想要一个函数 - 称之为fn-pow - 它将函数f应用于将f应用于参数的结果,然后将其应用于将其应用于其结果等的结果, n次。(fn-pow inc 0 3) 次。例如,

(inc (inc (inc 0)))

等同于

iterate

使用(defn fn-pow-0 [f x n] (nth (iterate f x) n))

可以轻松完成此操作
(defn fn-pow-1
  [f x n]
  (if (> n 0) 
    (recur f (f x) (dec n))
    x))

但是这会产生并抛弃不必要的懒惰序列。

从头开始编写函数并不难。这是一个版本:

fn-pow-0

(fn-pow inc 0 10000000)上使用Criterium,我发现这几乎是fn-pow-1的两倍。

我不认为fn-pow的定义是单一的,但{{1}}似乎可能是标准的内置函数,或者可能有一些简单的方法来定义它有一些巧妙安排的高阶函数。我也没有成功发现任何一个。我错过了什么吗?

4 个答案:

答案 0 :(得分:7)

您正在寻找的内置版可能是dotimes。我会以圆润的方式告诉你为什么。

时间

您在基准测试中测试的主要是间接级别的开销。那个(nth (iterate ...) n) 只是的两倍,当身体是一个非常快的函数时编译成循环的速度是相当令人惊讶/鼓舞的。如果f是一个代价更高的函数,那么开销的重要性会降低。 (当然如果你的f是低级别且速度很快,那么你使用低级循环结构。)

假设您的功能需要~1 ms而不是

(defn my-inc [x] (Thread/sleep 1) (inc x))

然后这两个大约需要1秒钟 - 差异大约是2%而不是100%。

(bench (fn-pow-0 my-inc 0 1000))
(bench (fn-pow-1 my-inc 0 1000))

空间

另一个问题是iterate正在创建一个不必要的序列。但是,如果您没有抓住头部,只需执行nth,那么您 ,并丢弃LazySeq个对象。换句话说,您使用的是一定数量的空间,尽管与n成比例地生成垃圾。但是,除非您的f是原始的或变异其参数,否则已经在生成自己的中间结果时与n成比例地生成垃圾。

减少垃圾

fn-pow-0fn-pow-1之间的有趣折衷方案是

(defn fn-pow-2 [f x n] (reduce (fn [x _] (f x)) x (range n)))

由于range个对象知道如何智能地减少自身,因此不会与n成比例地创建附加垃圾。它归结为一个循环。这是范围的reduce方法:

public Object reduce(IFn f, Object start) {
    Object ret = f.invoke(start,n);
    for(int x = n+1;x < end;x++)
            ret = f.invoke(ret, x);
    return ret;
}

这实际上是三个中最快的(在n版本的recur上添加原始类型提示之前)my-inc放慢了速度。

突变

如果你正在迭代一个在时间或空间上可能很昂贵的函数,比如矩阵运算,那么你很可能想要(以一种包含的方式)使用f来改变它的参数以消除垃圾高架。由于突变是副作用,并且您希望副作用n次,dotimes是自然的选择。

为了举例,我将使用atom作为替身,但想象一下在可变矩阵上进行抨击。

(def my-x (atom 0))

(defn my-inc! [x] (Thread/sleep 1) (swap! x inc))

(defn fn-pow-3! [f! x n] (dotimes [i n] (f! x)))

答案 1 :(得分:3)

这听起来就像编写函数n次一样。

(defn fn-pow [f p t]
    ((apply comp (repeat t f)) p))

答案 2 :(得分:2)

嗯。我注意到Ankur的版本比原版快了大约10倍 - 可能不是意图,无论多么惯用? : - )

仅仅针对计数器输入提示fn-pow-1对我来说产生了明显更快的结果 - 大约快了3倍。

(defn fn-pow-3 [f x ^long n]
    (if (> n 0)
       (recur f (f x) (dec n))
        x))

这大约是直接使用inc的版本的两倍,失去了可变性(不暗示x以保持测试精神)......

(defn inc-pow [x ^long n]
    (if (> n 0)
       (recur (inc x) (dec n))
        x))

我认为fn-pow-3可能是最好的解决方案。

我没有找到一种特别“惯用”的方式,因为它不像微基准之外的常见用例(尽管会喜欢被反驳)。

如果你有一个现实世界的例子,会很感兴趣吗?

答案 3 :(得分:1)

对于我们愚蠢的命令式程序员,更通用的模式称为 while 语句。我们可以在宏中捕获它:

(defmacro while [bv ; binding vector
                 tf ; test form
                 recf ; recur form
                 retf ; return form
                 ]
  `(loop ~bv (if ~tf (recur ~@recf) ~retf)))

...在你的情况下

(while [x 0, n 3] (pos? n)
  [(inc x) (dec n)]
  x)
; 3

  • 我希望键入提示n,但这是非法的。也许是的 推断。
  • 原谅我(重新)使用while
  • 这不太正确:它不允许在重复表单之前进行计算。

我们可以在重复之前调整宏来做事情:

(defmacro while [bv ; binding vector
                 tf ; test form
                 bodyf ; body form
                 retf ; return form
                 ]
  (let [bodyf (vec bodyf)
        recf (peek bodyf)
        bodyf (seq (conj (pop bodyf) (cons `recur recf)))]
    `(loop ~bv (if ~tf ~bodyf ~retf))))

例如

(while [x 0, n 3] (pos? n)
  (let [x- (inc x) n- (dec n)] [x- n-])
  x)
; 3

我觉得这很有表现力。因人而异。