你已经学会用零散的片段拼出一个引用并运行它。引用本身就是一段代码序列,所以你熟悉的那套序列操作词同样适用于它:
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
append 把两个引用合并成一个,concat 把一组引用展平,map 把数据变成引用,>quotation 让结果可以被调用。MACRO: 则把这项技能原封不动地搬到编译时。
一个 MACRO: 定义看起来和普通的词没什么两样,但它的函数体是在程序被编译的过程中运行的,而不是在程序运行时。函数体必须往栈上留下一个引用。接着编译器会把这个引用插入到调用处:它用函数体生成的代码替换掉宏调用。到了运行时,宏已经不复存在,只剩下生成出来的代码。
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
无论你在哪里写 [ 1 + ] twice,编译器都会:
[ 1 + ] 运行函数体,dup append 构造出引用 [ 1 + 1 + ],1 + 1 + 插入到那个位置。因此,一个调用了 twice 的词,编译出来的结果就和你手写 1 + 1 + 完全一样:
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
关键的区别在于:普通词接收一个引用,在运行时用 call 调用它。而宏从不调用任何东西,它交给编译器的是新代码,让编译器用这些代码取代自己。
因为函数体在编译时运行,所以宏的输入在那个时刻就必须是已知的。它们在调用处必须是字面量:一个 [ … ] 引用、一个数字、一个数组、一个字符串。只在运行时才存在的值无法交给宏。
! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot ! ok: the quotation and the number are literals
宏的函数体就是一段返回引用的普通代码,大部分工作都由你已经见过的那些词完成:
| 要构建… | 使用 |
|---|---|
从 [ A ] 和 [ B ] 得到 [ A B ]
|
append |
| 从一组引用得到一个引用 | concat >quotation |
| 为序列的每个元素生成一个引用 |
map,然后 concat >quotation
|
还需要来自 sequences 和 kernel 的两个词,才能凑齐这套工具。
<repetition>(位于 sequences 中)会生成由同一个元素复制 n 份组成的序列。它的栈效果是 ( length elt -- repetition ),其中计数在前。结果是虚拟序列,所以要用通常的方式把它展平:
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry(位于 kernel 中)会把一个值加到引用的前面,生成一个新的引用,该引用会先把这个值压栈:
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
curry 就是把某个特定元素固化进引用的办法,它正好能把 { 10 20 30 } 和 [ + ] 变成 [ 10 + ] [ 20 + ] [ 30 + ]。
有些语言里的宏需要特殊的引用/反引用语法,还需要额外的机制来避免命名冲突。Factor 完全不需要这些:引用本身就是一段由已解析的词组成的序列,解析器在读到 [ … ] 时就已经把 dup、+ 和 append 都查好了。所以宏的函数体不过是你早就会做的序列操作,只是它正好返回一个引用。
$[ … ]
宏返回一个引用,用来插入到调用中。如果你想在解析时计算出一个普通的值,就可以用 $[ code ]:它在文件被读取时运行 code,并把结果作为字面量折进源码里:
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ] 来自 literals 词表,这个词表汇集了把解析时的结果插值成字面量的那些词。这里提到它只是为了完整。你要写的四个模板(twice、repeat-quot、compose-many 和 each-literal)都是 MACRO: 声明。
你在维护一套信号站的代码生成工具。 每个宏都是一块模板,也就是编译器在每个调用点用来生成特化代码的小模板。
你只需要写这四个宏。测试文件定义了几个小的包装词,这些包装词会调用你的宏(宏只能通过它生成的代码来观察),然后检查它们运行时的结果。包装词保持原样即可。
在每个任务里,宏体都会在栈上收到调用点传入的字面量输入,最后必须留下一个引用。注释里标着“编译为”的那几行,展示的就是你的引用应当生成的直线代码。
twice接收一个字面量引用,返回一个把原引用连续运行两次的引用。
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quot接收一个字面量引用和一个字面量计数n,返回一个把原引用连续运行n次的引用。n = 0时返回空引用[ ]。
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-many接收一个由引用组成的字面量数组,返回一个单独的引用,依次运行其中的每一个引用。传入空数组时返回空引用。
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literal接收一个字面量序列和一个引用。它返回的引用会对序列中的每个元素做两件事:先把该元素压入栈,再运行那个引用。这样迭代就发生在编译期,生成的代码也是直线代码。
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60
这是四个任务里最讲究结构的一个:序列的长度和它的元素值都会影响生成的代码。提示会带你一步一步把它做出来。