Scheme宏的二三事

一次造轮子的经历

因为我的Scheme入门书是Dan Friedman的《The Little Schemer》,老爷子从头到尾都没有提到过if。所以一直到翻完整本书,对于只有2个分支的逻辑,我也是用这么“蠢萌”的方式写的(这里的cond可以理解成if的多分支的版本):

(define (my-abs x)
  (cond ((>= x 0) x)
            (else (- x))))

所谓杀鸡焉用牛刀,既然“if-then-else”的简洁性摆在那里,造轮子的事肯定也是干过的:

(define (my-if predicate then-clause else-clause)
  (cond (predicate then-clause)
            (else else-clause)))

通过自定义my-if函数,我们自己实现了if的功能。来测试一下:

(my-if (= 1 2) 0 1)
;Value: 1

(my-if (= 1 1) 0 1)
;Value: 0

scheme的波兰表达式写法,让自定义函数和默认关键字之间没有明显的区分。接下来就可以把最开始的my-abs改写的简短一点:

(define (my-abs x)
  (my-if (>= x 0) x (- x)))

在很长一段时间里这个my-if都可以很好的运行,直到有一天...

应用序和正则序

事情是这样的:之前替换的都是比较简单的场景,而这一次在cond中存在一个递归的函数调用,当再次运行的时候,发现程序无法退出了。下面是这个例子的简化版本,原始版本参考SICP[1]的32页。

; 某个递归调用自己的函数
(define (bomb) (bomb))

; 某个可以正常执行的代码片段
(cond ((= 1 1) 1)
      (else (bomb)))

; 陷入无限递归
(my-if (= 1 1) 1 (bomb))

让我们在仔细审视一下cond的求值过程:解释器会按顺序判断谓词逻辑是否成立,一旦(= 1 1)返回的结果为true,那么立刻返回1,并且 忽略 后续所有的谓词和对应的表达式,也就是说(bomb)根本没机会执行。

my-if其实是一个函数调用,遵循2种可能的求值方式:应用序(applicative-order)或者正则序(normal-order)。

先看一个直观一点的例子[2]

(define (double x)
    (+ x x))

(double (+ 2 1))

对于应用序,会首先求出(+ 2 1)的值,然后带入double的定义中,也就是说+一共执行了 2 次,1次是计算(+ 2 1),1次是计算(+ 3 3)

对于正则序,则会先展开double的定义,整个表达式变成(+ (+ 2 1) (+ 2 1)),然后对于(+ 2 1)分别求值 2 次,最后再相加,整个过程中+一共执行了 3 次。

再回过头看之前所遇到的问题,如果采用正则序,在遇到my-if时会先将其展开,然后再将参数带入,也就是和之前直接用cond写的没有区别。而应用序则相反,会先对于参数求值,然后再执行函数调用。也就是说,无论my-if是用什么方式定义的,(= 1 1)1(bomb)都会被先求值,而当(bomb)被求值时程序就陷入无限递归了。

我知道的几乎所有的编程语言(包括流行的Java、Python,也包括不太流行的Scheme)都选择了应用序作为求值方式。这也导致了我之前甚至连什么是正则序都不知道(至于正则序、惰性求值之类易混淆的概念,后面有机会再写一篇)。

再造一次轮子

在这次失败的经历后的某天,结合另一个我之前一直想不明白的问题:宏到底有什么作用?我突然就经历了一个“尤里卡时刻”:为什么不试试用宏来重新定义my-if

(define-syntax my-if
  (syntax-rules ()
    ((my-if predicate then-clause else-clause)
    (cond (predicate then-clause)
          (else else-clause)))))

通过define-syntax我们定义了一个宏,语法的部分我不做赘述。可以看到,通过把my-if展开为cond对应的表达式,我们实现了需要的功能,因为宏展开是基于正则序的

顺带一提的是,我们通过宏,几句话就实现了自定义语法,这也正是Lisp系语言元编程的能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章