次元超越! Macro in Racket
Intro
最近写代码感觉非常的不顺。 之前看自己一个月以来写的代码真的是被自己恶心到了。 感觉我的Racket 还是没有能够掌握主要特性。 所以大概就花了一周左右的时间把Macro拿出来学了一下, 今年的PL课上很可惜没有时间在课上讲这个。
如果说编程语言是魔咒学的话, 这个Macro 绝对是次元魔法, 特别是在Racket语言当中。 简单的说, Macro就是能让你在正式编译之前的,先对你的代码进行一次预编译, 在这个时候把你代码中的一些符合条件的规则,全部替换成你需要的代码,或者说去生成你想要的代码。 之前写过C语言的宏的话应该还是对这个东西不陌生的,但是在大部分的语言中,宏只能做一些非常Ad-hoc的替换,如果你的替换方式非常的复杂, 这时候就会很难写,也很容易出Bug。但是在Racket之类的Scheme系语言当中,宏系统基本上就是能让你直接在编译期运行你写的scheme/racket代码。得益于这种强大的能力你可以轻松进入高次元,在那里直接对racket这个编程语言本身进行扩展得到你想要的语法特性。Racket 是 Racket的meta language
交换
在编程中我们经常会需要去交换两个变量的值。这个在命令式语言中非常的简单, 可以直接用/
temp = a;
a = b;
b = tmp;
在racket当中我们也不难能够写出下面的code
(let ([tmp x])
(set! x y)
(set! y tmp))
虽然racket作为函数式语言这种命令式编程语言中的“变量”,其实在racket中非常的少用。
写过汇编的都知道其实汇编里面描述交换的时候可以直接使用XCHG
。这个语义其实非常的好, 我们希望在我们的编程语言中能够实现这一种语法结构。根据metacircular interpretor 的思想,如果我们把racket当作racket本身的meta language, 我们需要做的就是把xchg
这个语法“翻译”到上面的这这段racket code。
这种替换非常的直接, 我们可以用define-syntax-rule
来直接在我们的代码中给racket添加一条语法规则
(define-syntax-rule (xchg x y)
(let ([tmp x])
(set! x y)
(set! y tmp)))
当然你如果把上面的define-syntax-rule
改成define
代码的运行结果不会发生任何的改变。
那和函数有什么区别?
其实这里编译器就相当于在编译值之前先把你所有代码中的xchg 修改成了(let ...)你的代码本身发生的变化,而如果定义函数的方式来实现,这里实际上就是一个函数调用,会新建一个函数栈在运行时,但是你的代码一旦写好了,自然是不会被编译器修改的。
这里很明显的感觉就是使用s 表达式的优势, 因为我们的代码相当于已经天然的被parse好了,所以很容易看清语法的边界, 这种代码和数据(list)的同构性,让写宏的人会更加清晰。
使用racket编写语法谓词
如果我们要使用racket来写一个简单的解释器,第一步需要做的就是定义目标语言的predicate, 这样我们就可以配合quasi quoting和pattern match来比较轻松的定义语言的语法了,比如简单的lambda的语法如下
lambda-exp :: x
| (lambda-exp lambda-exp)
| (lamdba (x ...) lambda-exp)
where x is symbol
使用racket predicate来描述
(define (lambda-exp? e)
(match e
[(? symbol?) #t]
[`(lambda (,x ...) ,(? lambda-exp?)) #t]
[`(,(? lambda-exp? e1) ,(? lambda-exp?)) #t]))
不过感觉这一学期下来给我的感受就是,其实大部分人对于quasi quoting pattern match的阅读能力几乎是0........很多人到了期末都没彻底搞明白。
这个其实是一种在racket当中比较常见的做法但是确实会带来一些的阅读困难,同时这种语法结构其实并不是那么容易去扩展的, 在简单单一的一组EBNF规则中其实是看不太出这个。但是如果我希望做到类似与extend一组规则的时候这种结构其实就会显的比较乱了。 如果我们是要写一个编译器,在常见的教程和方法中都会要求有非常多级的IR,特别是如果我们使用nano-pass style, 就需要非常对级不同的IR,每级只是对中间的某一个小部分进行修改,这时候纯使用predicate就会有点难受了。
这时候我们可以使用宏,在racket中定义一个语法结构叫define-ir
, 我们可以编写类似EBNF的代码,然后macro expand之后我们就可以自动得到我们想要的predicate
使用宏生成语法谓词
1. syntax
这个其实还是比较复杂的,可以一点一点来, 先看一种比较简单的情况:
我们的宏会take 一个list,最终在编译之后把list上的所有终结符(terminate symbol) 全部替换成quasi pattern,也就是说比如
'(symbol symbol)
在 expand 之后会变成 `(,(? symbol?) ,(? symbol?))
对于宏我们的本质就是把代码结构就像list一样进行演算。尽管我们的s表达式非常的像list,但是很明显'(symbol symbol)
只是一个list data, 如何在racket表示一段代码'(symbol symbol)
呢?
其实和quote有点像我们可以使用syntax quote,#'关键字
#'(symbol symbol)
这样表示的就是一段syntax对象而不是一个list对象了。
我们甚至还可以使用quasi syntax, 这样我们就可以在一个syntax object中间使用#,来混入racket代码了
(define x #'c)
#`(a b #,x)
我们也可以用racket的内置函数synatx->datum
\ datum->syntax
来完成这种data 和syntax的互相转化
> (require racket/syntax)
> (syntax->datum #'symbol?)
'symbol?
> (syntax->datum #'(a b c))
'(a b c)
> (datum->syntax (current-syntax-context) 'x)
#<syntax x>