Scheme 局部变量
Scheme 提供了几种局部变量的定义方式,本文简要讨论
define
let
letrec
let*
这几种。
和命令式语言中的局部变量不同,Scheme 的局部变量总是某个函数的参数,本文介绍的几种形式都是某种函数定义模式的语法糖。
解释几个名词:
- 环境:是指 scheme 解释器求值表达式的时候生成的局部变量作用域。全局环境是解释器创建的第一个环境,之后,每次函数调用创建一个新环境,新环境与生成它的环境之间有嵌套关系
- 闭包函数:嵌套定义的函数
- 键值:将环境看作从键到值的映射
- 匿名函数:就是用 lambda 定义的函数
define
define 在其所处环境中绑定键值,按顺序求值。
(define a 34)
(define b (* 3 a))
> b
102
let
语法
let 表达式的一般形式:
(let ((<var> <exp>) ...)
<body>)
等价形式
相当于定义函数再带入实参:
(let ((a 34))
(* 3 a))
> 102
; 相当于
((lambda (a) (* 3 a))
34)
> 102
letrec
动机
有些时候,函数互相调用:
(define ieven? (lambda (n)
(if (zero? n)
#t
(iodd? (- n 1)))))
(define iodd? (lambda (n)
(if (zero? n)
#f
(ieven? (- n 1)))))
(ieven? 3)
> #f
Scheme 定义函数时不会去扫描函数体,所以可以在 ieven?
中调用 iodd?
。
语法
使用 letrec
:
(letrec ((ieven? (lambda (n)
(if (zero? n)
#t
(iodd? (- n 1)))))
(iodd? (lambda (n)
(if (zero? n)
#f
(ieven? (- n 1))))))
(ieven? 3))
等价形式
错误的
其等价形式是什么呢?不能直接按 let
的套路定义闭包,比如下面的代码无法运行(如果你试图检验这一点,注意你的全局环境中不能有 ieven?
iodd?
):
((lambda (ieven? iodd?) (ieven? 3))
(lambda (n)
(if (zero? n)
#t
(iodd? (- n 1))))
(lambda (n)
(if (zero? n)
#f
(ieven? (- n 1)))))
报错:
iodd?: undefined;
cannot reference an identifier before its definition
我们分析一下:第一个 lambda 位于全局环境,有两个形参,随后的两个 lambda 是其实参。实参的求值环境(本例中是全局环境)中没有 iodd?
,所以报错。你可能会说:iodd?
不是第一个 lambda 的形式参数吗?没错,但是形式参数的作用域仅限自己的函数体,本例中只有(ieven? 3)
这一小段。
正确的
所以 letrec
相当于加了一层环境,这是它和 let
的区别。
((lambda ()
(define (ieven? n)
(if (zero? n)
#t
(iodd? (- n 1))))
(define (iodd? n)
(if (zero? n)
#f
(ieven? (- n 1))))
(ieven? 3)))
let*
语法与等价形式
在命令式编程中,一个局部变量经常依赖于另一个局部变量,就像本文开始时的例子:
(define a 34)
(define b (* 3 a))
> b
102
相当于嵌套闭包:
((lambda (a)
((lambda (b)
b)
(* 3 a)))
34)
> 102
实例
举个实际一点的例子:
(define (list->tree elements)
(car (partial-tree elements (length elements))))
(define (partial-tree elts n)
(if (= n 0)
(cons '() elts)
(let* ((left-size (quotient (- n 1) 2))
(left-result (partial-tree elts left-size))
(left-tree (car left-result))
(non-left-elts (cdr left-result))
(right-size (- n (+ left-size 1)))
(this-entry (car non-left-elts))
(right-result (partial-tree (cdr non-left-elts) right-size))
(right-tree (car right-result))
(remaining-elts (cdr right-result)))
(cons (make-tree this-entry left-tree right-tree)
remaining-elts))))
这里 let*
做了很多中间结果命名的工作,增强了可读性,可能也避免了重复计算(一些中间结果参与到多个后续计算中)。注意,let*
只能按顺序求值,想想它嵌套定义的等价形式。
总结
这几种方式与直接 define 的区别是利用了闭包函数,键值绑定都发生在闭包里,不像 define 绑定的变量在 define 所在的作用域里。
let
letrec
let*
之间的区别就在于“传参”求值的过程:
let
和定义一层函数,所以参数只能引用所处作用域中的变量letrec
定义两层函数,让参数求值时可以互相引用,当然只有在参数是函数的时候才和let
有区别let*
嵌套定义函数,每个参数定义一个
本文参考了 Scheme r5rs letrec的用法
转载:https://blog.csdn.net/weixin_42156883/article/details/104660912