Scheme 局部变量

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的用法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值