循环形式的命名 let 如何工作?
How does the named let in the form of a loop work?
在解释如何将数字转换为列表的 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)))))
这里使用了“named let
”。我不明白这个名为 let
的工作原理。
我看到定义了一个循环,其中变量 n
等于 n
,变量 acc
等于空列表。然后,如果 n
小于 10,则 n
被授予 acc。否则,"the loop" 应用 n
等于 n/10
和 acc
等于 n
/10 的余数和先前累积的东西的余数,并且然后调用自己。
我不明白loop
为什么叫循环(什么是循环?),它是如何自动执行和调用自己的,以及它实际上是如何将每个数字乘以其适当的乘数相加形成以 10 为基数的数字。
我希望有人能对程序和上述问题有所启发,以便我更好地理解它。谢谢。
命名 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。使用尾递归,递归调用 returns 直接到您的调用者,因此无需保留当前调用帧。您可以根据需要多次执行尾递归,而不会导致堆栈溢出。这样,它就像一个循环一样工作。
如果您想了解有关 named let
及其工作原理的更多信息,I wrote a blog post about it。 (不过,你不需要阅读它来理解这个答案。如果你好奇的话,它就在那里。)
正常的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)
请注意,letrec
的主体只是计算命名过程,因此该名称不在第一次调用的环境中。因此你可以这样做:
(let ((loop 10))
(let loop ((n loop))
(if (zero? n)
'()
(cons n (loop (- n 1))))) ; ==> (10 9 8 7 6 5 4 3 2 1)
过程loop
只存在于内部let
的body环境中,不隐藏外部let
的变量loop
。
在您的示例中,名称 loop
只是一个名称。在 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)))))
这里使用了“named let
”。我不明白这个名为 let
的工作原理。
我看到定义了一个循环,其中变量 n
等于 n
,变量 acc
等于空列表。然后,如果 n
小于 10,则 n
被授予 acc。否则,"the loop" 应用 n
等于 n/10
和 acc
等于 n
/10 的余数和先前累积的东西的余数,并且然后调用自己。
我不明白loop
为什么叫循环(什么是循环?),它是如何自动执行和调用自己的,以及它实际上是如何将每个数字乘以其适当的乘数相加形成以 10 为基数的数字。
我希望有人能对程序和上述问题有所启发,以便我更好地理解它。谢谢。
命名 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。使用尾递归,递归调用 returns 直接到您的调用者,因此无需保留当前调用帧。您可以根据需要多次执行尾递归,而不会导致堆栈溢出。这样,它就像一个循环一样工作。
如果您想了解有关 named let
及其工作原理的更多信息,I wrote a blog post about it。 (不过,你不需要阅读它来理解这个答案。如果你好奇的话,它就在那里。)
正常的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)
请注意,letrec
的主体只是计算命名过程,因此该名称不在第一次调用的环境中。因此你可以这样做:
(let ((loop 10))
(let loop ((n loop))
(if (zero? n)
'()
(cons n (loop (- n 1))))) ; ==> (10 9 8 7 6 5 4 3 2 1)
过程loop
只存在于内部let
的body环境中,不隐藏外部let
的变量loop
。
在您的示例中,名称 loop
只是一个名称。在 Scheme 中,每个循环最终都是通过递归完成的,但通常在尾递归时使用名称,因此是一个迭代过程。