以循环形式命名的let如何工作?

时间:2015-08-09 21:42:03

标签: scheme

an answer中解释了如何将数字转换为列表,number->list过程定义如下:

(define (number->list n)
  (let loop ((n n)
             (acc '()))
    (if (< n 10)
        (cons n acc)
        (loop (quotient n 10)
              (cons (remainder n 10) acc)))))

这里有&#34; named let&#34;用来。我不明白这个名为let的工作原理。

我看到定义了一个循环,其中变量n等于n,变量acc等于空列表。然后,如果n小于10,则n与acc一致。否则,&#34;循环&#34;应用n等于n/10acc等于n / 10剩余部分的缺点以及之前累积的内容,然后自行调用。

我不明白为什么loop被称为循环(什么是循环?),它如何自动执行和调用自身,以及它将如何实际添加每个数字乘以其适当的乘数来形成基数为10的数字。

我希望有人能够对程序和上述问题发表意见,以便我能更好地理解它。感谢。

2 个答案:

答案 0 :(得分:11)

命名let背后的基本思想是,它允许您创建一个可以调用自身的内部函数,并自动调用它。所以你的代码相当于:

(define (number->list n)
  (define (loop n acc)
    (if (< n 10)
        (cons n acc)
        (loop (quotient n 10)
              (cons (remainder n 10) acc))))
  (loop n '()))

希望您更容易阅读和理解。

然后,您可能会问为什么人们倾向于使用命名的let而不是定义内部函数并调用它。它与人们使用(未命名)let的理由相同:它将两个步骤(定义一个函数并调用它)转换为一个方便的形式。

它被称为循环,因为该函数在尾部位置中调用自身。这称为tail recursion。通过尾递归,递归调用直接返回给调用者,因此不需要保持当前的调用框架。您可以根据需要多次执行尾递归,而不会导致堆栈溢出。这样,它就像循环一样工作。

如果您想了解有关已命名的let及其工作原理的更多信息,请I wrote a blog post about it。 (但是,你不需要阅读它来理解这个答案。如果你感到好奇,它就在那里。)

答案 1 :(得分:3)

正常let用法可以被视为匿名过程调用:

(let ((a 10) (b 20))
  (+ a b))

;; is the same as 
((lambda (a b)
   (+ a b))
 10
 20)

命名let只是将该过程绑定到过程范围内的名称,以使其等于单个过程letrec

(let my-chosen-name ((n 10) (acc '()))
  (if (zero? n)
      acc
      (my-chosen-name (- n 1) (cons n acc)))) ; ==> (1 2 3 4 5 6 7 8 9 10)

;; Is the same as:
((letrec ((my-chosen-name 
          (lambda (n acc)
            (if (zero? n)
                acc
                (my-chosen-name (- n 1) (cons n acc))))))
  my-chosen-name)
 10
 '()) ; ==> (1 2 3 4 5 6 7 8 9 10)

请注意,letrect的主体只是对命名过程进行评估,以便该名称不在第一次调用的环境中。因此你可以这样做:

(let ((loop 10))
  (let loop ((n loop))
    (if (zero? n)
        '()
        (cond n (loop (- n 1))))) ; ==> (10 9 8 7 6 5 4 3 2 1)

过程loop仅在let的主体环境中,并且在首次评估loop时不会影响变量let

在您的示例中,名称loop只是一个名称。在Scheme中,每个循环最终都是通过递归完成的,但通常在尾递归时使用名称,因此也就是迭代过程。