在函数 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) )
对于您描述的简化定义,这两种形式实际上是完全等价的。唯一的区别是,使用直接递归的那个在每次迭代时传递 f
、init
和 l
,而使用命名 let
的那个只传递 init
和 l
。但是,我认为这种差异完全可以忽略不计。
但是,该简化版本并不是答案顶部提到的实际 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 中不会对速度产生太大影响。
语言内部通常看起来有点神秘,所以我很惊讶实现如此简单。我敢打赌真正的工作是球拍优化器,这也有利于我们的用户代码。这三个版本在字节码中可能变得无法区分。
从
(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) )
对于您描述的简化定义,这两种形式实际上是完全等价的。唯一的区别是,使用直接递归的那个在每次迭代时传递 f
、init
和 l
,而使用命名 let
的那个只传递 init
和 l
。但是,我认为这种差异完全可以忽略不计。
但是,该简化版本并不是答案顶部提到的实际 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 中不会对速度产生太大影响。
语言内部通常看起来有点神秘,所以我很惊讶实现如此简单。我敢打赌真正的工作是球拍优化器,这也有利于我们的用户代码。这三个版本在字节码中可能变得无法区分。