好的,所以您正在寻找不使用状态变量和尾调用的答案。您需要一个递归 procedure,它也演变出一个递归 process。不知道为什么除了看看定义会有什么不同之外,你还想要这个。您还应该阅读 tail recursion modulo cons(here 和 on 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)
如果你使用compose 和curry,它会稍微清理一下:
;; 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)