【问题标题】:Building the built-in procedure "build-list" in Racket在 Racket 中构建内置程序“build-list”
【发布时间】:2017-03-31 04:51:45
【问题描述】:

我正在尝试在 Racket 中构建内置程序 build-list

内置函数是这样工作的:

(build-list 10 (lambda (x) (* x x)))

>> '(0 1 4 9 16 25 36 49 64 81)

我的实现是递归过程的递归定义:

(define (my-build-list-recur list-len proc)
  (if (= list-len 0)
      '()
      (cons  (proc (sub1 list-len)) (my-build-list-recur (sub1 list-len) proc))))

当我调用我的实现时,我有:

(my-build-list-recur 10 (lambda (x) (* x x)))
>> '(81 64 49 36 25 16 9 4 1 0)

您可能已经看到,我得到了相同的结果,但顺序相反。

如何使结果与本机函数的顺序相同?

P.S.:我已经使用递归定义实现了一个完美运行的迭代过程。我现在正在努力用完全递归的过程生成相同的结果。我已经知道如何用长尾递归解决这个疑问。

这是我的长尾递归实现:

(define (my-build-list list-len proc)
  (define (iter list-len accu n)
    (if (= (length accu) list-len)
        (reverse accu)
        (iter list-len (cons (proc n) accu) (add1 n))))
  ;(trace iter)
  (iter list-len '() 0))

【问题讨论】:

  • 我会使用从0list-len - 1 的辅助函数。
  • 您可以简单地编写一个包装过程,调用递归过程,然后反转结果。
  • 如果您想模仿它的行为,请查看它的实现方式in racket's source
  • 你一直在说“长”尾递归。你认为“长”是什么意思?它与(非长)尾递归有何不同?
  • 嗯,我也在读 SICP,但我不记得“尾递归”和“长尾递归”之间的任何区别——但我记得递归过程(非尾位置的递归) ) 与线性迭代过程(尾部位置递归)。我会相应地更新我的答案。

标签: recursion iteration lisp racket tail-recursion


【解决方案1】:

好的,所以您正在寻找不使用状态变量和尾调用的答案。您需要一个递归 procedure,它也演变出一个递归 process。不知道为什么除了看看定义会有什么不同之外,你还想要这个。您还应该阅读 tail recursion modulo conshereon wikipedia)——它与这个问题有关。

;; recursive procedure, recursive process
(define (build-list n f)
  (define (aux m)
    (if (equal? m n)
        empty
        (cons (f m) (aux (add1 m)))))
  (aux 0))

(build-list 5 (λ (x) (* x x)))
;; => '(0 1 4 9 16)

注意aux 调用如何不再处于尾部位置——即cons 在其参数中评估aux 调用之前无法完成评估。这个过程看起来像这样,在堆栈上不断发展:

(cons (f 0) ...)
(cons (f 0) (cons (f 1) ...))
(cons (f 0) (cons (f 1) (cons (f 2) ...)))
(cons (f 0) (cons (f 1) (cons (f 2) (cons (f 3) ...))))
(cons (f 0) (cons (f 1) (cons (f 2) (cons (f 3) (cons (f 4) ...)))))
(cons (f 0) (cons (f 1) (cons (f 2) (cons (f 3) (cons (f 4) empty)))))
(cons (f 0) (cons (f 1) (cons (f 2) (cons (f 3) (cons (f 4) '())))))
(cons (f 0) (cons (f 1) (cons (f 2) (cons (f 3) '(16)))))
(cons (f 0) (cons (f 1) (cons (f 2) '(9 16))))
(cons (f 0) (cons (f 1) '(4 9 16)))
(cons (f 0) '(1 4 9 16))
'(0 1 4 9 16)

您会看到cons 调用保持打开状态,直到... 被填写。最后一个... 没有填写empty,直到m 等于n .


如果您不喜欢内部aux 过程,您可以使用默认参数,但这确实会将一些私有API 泄​​漏到公共API。也许它对你有用和/或也许你并不在乎。

;; recursive procedure, recursive process
(define (build-list n f (m 0))
  (if (equal? m n)
      '()
      (cons (f m) (build-list n f (add1 m)))))

;; still only apply build-list with 2 arguments
(build-list 5 (lambda (x) (* x x)))
;; => '(0 1 4 9 16)

;; if a user wanted, they could start `m` at a different initial value
;; this is what i mean by "leaked" private API
(build-list 5 (lambda (x) (* x x) 3)
;; => '(9 16)

堆栈安全实施

为什么你特别想要一个递归过程(一个增长堆栈的过程)很奇怪,imo,特别是考虑到编写一个不会增长堆栈的堆栈安全build-list 过程是多么容易。下面是一些带有线性迭代过程的递归过程。

第一个非常简单,但使用acc 参数确实泄漏了一些私有API。您可以使用aux 过程轻松解决此问题,就像我们在第一个解决方案中所做的那样。

;; recursive procedure, iterative process
(define (build-list n f (acc empty))
  (if (equal? 0 n)
      acc
      (build-list (sub1 n) f (cons (f (sub1 n)) acc))))

(build-list 5 (λ (x) (* x x)))
;; => '(0 1 4 9 16)

查看进化过程

(cons (f 4) empty)
(cons (f 3) '(16))
(cons (f 2) '(9 16))
(cons (f 1) '(4 9 16))
(cons (f 0) '(1 4 9 16)) 
;; => '(0 1 4 9 16)

这非常好,因为它可以不断地重复使用一个堆栈帧,直到构建整个列表。作为一个额外的优势,我们不需要保留从 0 到 n 的计数器。相反,我们向后构建列表并从n-1 计数到0


最后,这是另一个演化线性迭代过程的递归过程。它使用命名让和延续传递风格。这次循环有助于防止 API 泄​​漏。

;; recursive procedure, iterative process
(define (build-list n f)
  (let loop ((m 0) (k identity))
    (if (equal? n m)
        (k empty)
        (loop (add1 m) (λ (rest) (k (cons (f m) rest)))))))

(build-list 5 (λ (x) (* x x)))
;; => '(0 1 4 9 16)

如果你使用composecurry,它会稍微清理一下:

;; recursive procedure, iterative process
(define (build-list n f)
  (let loop ((m 0) (k identity))
    (if (equal? n m)
        (k empty)
        (loop (add1 m) (compose k (curry cons (f m)))))))

(build-list 5 (λ (x) (* x x)))
;; => '(0 1 4 9 16)

从这个过程演变而来的过程略有不同,但您会注意到它也不会增加堆栈,而是在堆上创建一系列嵌套的 lambda。所以这对于足够大的n 值来说就足够了:

(loop 0 identity)                        ; k0
(loop 1 (λ (x) (k0 (cons (f 0) x)))      ; k1
(loop 2 (λ (x) (k1 (cons (f 1) x)))      ; k2
(loop 3 (λ (x) (k2 (cons (f 2) x)))      ; k3
(loop 4 (λ (x) (k3 (cons (f 3) x)))      ; k4
(loop 5 (λ (x) (k4 (cons (f 4) x)))      ; k5
(k5 empty)
(k4 (cons 16 empty))
(k3 (cons 9 '(16)))
(k2 (cons 4 '(9 16)))
(k1 (cons 1 '(4 9 16)))
(k0 (cons 0 '(1 4 9 16)))
(identity '(0 1 4 9 16))
'(0 1 4 9 16)

【讨论】:

  • 哈,几乎没有。别人在racket 标签上的回答总是让我感到谦卑。我仍然只是学习基础知识^_^
  • 这是一个精彩的答案,代码块上的格式显示出对细节的非常关注。另一件小事是 Racket 不使用 C 堆栈,它的堆栈可以有效地无限增长,所以你永远不会有“堆栈溢出”错误,你只会用完内存。这使得根据所讨论的算法以递归方式编写事情变得不那么重要了,尽管在某些情况下考虑它仍然很重要,而且不会有坏处!
  • 我在 TRMC 上的 WP 文章的链接中进行了编辑,我们可以在其中找到 Scheme 函数 duplicate,使用命名为 let "scheme-fu" 进行清理,正如您链接到的博客中所暗示的那样。
  • @AlexisKing 非常酷,我不知道堆栈溢出在球拍中很难“实现”。谢谢分享^_^
  • @WillNess 欢迎编辑。创建 TRMC 链接时,我想到了不同的来源,但似乎找不到。我选择了你最初看到的那个。感谢您清理评估可视化。ありがとう!
猜你喜欢
  • 2017-02-21
  • 2017-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-28
  • 1970-01-01
  • 2019-08-10
相关资源
最近更新 更多