在函数 Racket 中使用内部循环的优点

Advantage of using an inner loop in functions Racket

中获取 foldl 的定义,第一个函数比第二个函数有什么优势:

(define (oldfoldl f init l)
  (let loop ([init init] [l l])   
    (if (null? l)  init
        (loop (f (car l) init) (cdr l)))))

(define (myfoldl f init l)
  (if (null? l) init
      (myfoldl f (f (car l) init) (cdr l)) ))

换句话说,使用内部子函数有什么好处吗? 两者给出相同的结果:

(oldfoldl + 0 '(1 2 3 4))
(myfoldl + 0 '(1 2 3 4))

输出:

10
10

编辑:使用 define 而不是 let 的第三个版本如下。会不会不一样:

(define (myfoldl2 f init l)
  (define (loop [init init] [l l])   
    (if (null? l)  init
        (loop (f (car l) init) (cdr l)) ))
  (loop init l) ) 

对于您描述的简化定义,这两种形式实际上是完全等价的。唯一的区别是,使用直接递归的那个在每次迭代时传递 finitl,而使用命名 let 的那个只传递 initl。但是,我认为这种差异完全可以忽略不计。

但是,该简化版本并不是答案顶部提到的实际 foldl 函数,它看起来像这样:

(define foldl
  (case-lambda
    [(f init l)
     (check-fold 'foldl f init l null)
     (let loop ([init init] [l l])
       (if (null? l) init (loop (f (car l) init) (cdr l))))]
    [(f init l . ls)
     (check-fold 'foldl f init l ls)
     (let loop ([init init] [ls (cons l ls)])
       (if (pair? (car ls)) ; `check-fold' ensures all lists have equal length
           (loop (apply f (mapadd car ls init)) (map cdr ls))
           init))]))

这使用 case-lambda 执行参数计数分派,以及名为 check-fold 的内部函数执行参数类型检查。使用命名 let 而不是递归调用 foldl 可以避免在每次迭代时执行分派和运行时检查,这会产生更多的开销。

是的。通过保留不会因连续递归而改变的变量,您的眼睛会停留在您正在查看的特定行中重要的变量上。

使用命名的 let 只是定义过程然后使用它的好方法,但变量和值位于顶部。这使得更容易看到它是如何初始化的。

在你的例子中,我认为所有这些都没有问题,即使是第一个在连续递归中传递不变的变量时也是如此,但是对于更复杂的过程,删除不变的绑定并创建辅助过程有助于使其更容易理解。

closures/lambdas 应该是相当便宜的,所以在大多数情况下,将表达式包装在 let/procedure 中不会对速度产生太大影响。

语言内部通常看起来有点神秘,所以我很惊讶实现如此简单。我敢打赌真正的工作是球拍优化器,这也有利于我们的用户代码。这三个版本在字节码中可能变得无法区分。