newLISP 宏简介
我们已经介绍了 newLISP 的基础知识,但还有很多强大的功能有待发现。一旦掌握了语言的主要规则,就可以决定要添加哪些更高级的工具。你可能想探索的一个功能是 newLISP 提供的宏。
宏是一种特殊的函数类型,你可以使用它来改变 newLISP 对代码求值的方式。例如,你可以创建新的控制函数类型,比如你自己的 if 或 case 版本。
使用宏,你可以开始让 newLISP 按照你的意愿工作。
严格来说,newLISP 的宏是 fexprs,而不是宏。在 newLISP 中,fexprs 被称为宏部分是因为说 'macros' 比说 'fexprs' 简单得多,但主要是它们与其他 LISP 方言中的宏具有相似的目的:它们允许你定义 特殊形式,比如你自己的控制函数。
为了理解宏,让我们回到本介绍中的第一个例子。考虑这个表达式是如何求值的
(* (+ 1 2) (+ 3 4)) ;-> (* 3 7) ;-> 21
* 函数根本看不到 + 表达式,只看到它们的结果。newLISP 热情地对加法表达式进行了求值,然后只将结果传递给乘法函数。这通常是你想要的,但有时你不想立即对每个表达式求值。
考虑内置函数 if 的操作
(if (<= x 0) (exit))
如果 x 大于 0,则测试返回 nil,因此(exit)函数不会被求值。现在假设你想要定义你自己的 if 函数版本。它应该很容易
(define (my-if test true-action false-action)
(if test true-action false-action))
> (my-if (> 3 2) (println "yes it is" ) (exit)) yes it is $
但这不起作用。如果比较返回 true,newLISP 会打印一条消息,然后退出。即使比较返回 false,newLISP 仍然会退出,不会打印任何消息。问题在于(exit)在调用 my-if 函数之前就进行求值,即使你不想这样做。对于普通函数,参数中的表达式首先进行求值。
宏类似于函数,但它们允许你控制何时(以及是否)对参数进行求值。你使用 define-macro 函数来定义宏,就像你使用 define 来定义函数一样。这两个定义函数都允许你创建接受参数的函数。重要的区别是,对于普通的 define,参数在函数运行之前进行求值。但是当你调用用 define-macro 定义的宏函数时,参数会以原始的未求值形式传递给定义。你决定何时对参数进行求值。
my-if 函数的宏版本如下所示
(define-macro (my-if test true-action false-action)
(if (eval test) (eval true-action) (eval false-action)))
(my-if (> 3 2) (println "yes it is" ) (exit))
"yes it is"
test 和 action 参数不会立即求值,只有当你想要它们求值时,才会使用 eval。这意味着(exit)不会在进行测试之前求值。
这种推迟求值的能力使你能够编写自己的控制结构,并向语言添加强大的新形式。
newLISP 提供了一些用于构建宏的有用工具。除了 define-macro 和 eval 之外,还有 letex,它提供了一种在对表达式求值之前将局部符号扩展到表达式中的方法,以及 args,它返回传递给宏的所有参数。
编写宏时需要注意的一个问题是宏中的符号名称可能会与调用宏的代码中的符号名称混淆。这是一个简单的宏,它向语言添加了一个新的循环结构,该结构结合了 dolist 和 do-while。一个循环变量在条件为 true 时遍历列表
(define-macro (dolist-while)
(letex (var (args 0 0) ; loop variable
lst (args 0 1) ; list
cnd (args 0 2) ; condition
body (cons 'begin (1 (args)))) ; body
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
它像这样调用
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1)))
x is 19 x is 18 x is 17 x is 16 x is 15 x is 14 x is 13 x is 12 x is 11 x is 10
它似乎工作良好。但有一个细微的问题:你不能使用名为 y 的符号作为循环变量,即使你可以使用 x 或其他任何符号。将一个 (println y) 语句放入循环中看看原因
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1))
(println {y is } y))
x is 19 y is true x is 18 y is true x is 17 y is true
如果你尝试使用 y,它将无法工作
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is value expected in function dec : y
问题在于 y 被宏用来保存条件值,即使它在自己的 let 表达式中。它显示为一个 true/nil 值,因此不能递减。为了解决这个问题,将宏包含在一个上下文中,并使宏成为该上下文中的默认函数
(context 'dolist-while)
(define-macro (dolist-while:dolist-while)
(letex (var (args 0 0)
lst (args 0 1)
cnd (args 0 2)
body (cons 'begin (1 (args))))
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
(context MAIN)
它可以用相同的方式使用,但没有任何问题
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is 19 y is 18 y is 17
newLISP 用户发现许多不同的使用宏的原因。以下是我在新LISP 用户论坛上找到的几个宏定义。
这是一个称为 ecase(求值-case)的 case 版本,它确实对测试进行求值
(define-macro (ecase _v)
(eval (append
(list 'case _v)
(map (fn (_i) (cons (eval (_i 0)) (rest _i)))
(args)))))
(define (test n)
(ecase n
((/ 4 4) (println "n was 1"))
((- 12 10) (println "n was 2"))))
(set 'n 2)
(test n)
n was 2
你可以看到,除法(/ 4 4)和(- 12 10)都被求值了。在标准版本的 case 中,它们不会被求值。
这是一个创建函数的宏
(define-macro (create-functions group-name)
(letex
((f1 (sym (append (term group-name) "1")))
(f2 (sym (append (term group-name) "2"))))
(define (f1 arg) (+ arg 1))
(define (f2 arg) (+ arg 2))))
(create-functions foo)
; this creates two functions starting with 'foo'
(foo1 10)
;-> 11
(foo2 10)
;-> 12
(create-functions bar)
; and this creates two functions starting with 'bar'
(bar1 12)
;-> 13
(bar2 12)
;-> 14
以下代码更改了 newLISP 的操作,以便使用 define 定义的每个函数在求值时,会将它的名称和参数详细信息添加到日志文件中。运行脚本时,日志文件将包含已求值的函数和参数的记录。
(context 'tracer)
(define-macro (tracer:tracer farg)
(set (farg 0)
(letex (func (farg 0)
arg (rest farg)
arg-p (cons 'list (map (fn (x) (if (list? x) (first x) x))
(rest farg)))
body (cons 'begin (args)))
(lambda
arg
(append-file
(string (env "HOME") "/trace.log")
(string 'func { } arg-p "\n"))
body))))
(context MAIN)
(constant (global 'newLISP-define) define)
; redefine the built-in define:
(constant (global 'define) tracer)
要使用这个简单的跟踪器运行脚本,请在运行之前加载上下文
(load {tracer.lsp})
生成的日志文件包含一个列表,其中包含已调用的每个函数以及它接收的参数
Time:Time (1211760000 0) Time:Time (1230163200 0) Time:Time (1219686599 0) show ((Time 1211760000 0)) show ((Time 1230163200 0)) get-hours ((Time 1219686599 0)) get-day ((Time 1219686599 0)) days-between ((Time 1219686599 0) (Time 1230163200 0)) leap-year? ((Time 1211760000 0)) adjust-days ((Time 1230163200 0) 3) show ((Time 1230422400 0)) Time:Time (1219686599 0) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) period-to-string ((Duration 124.256956)) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) Time:print ((Time 1211760000 0)) Time:string ((Time 1211760000 0)) Duration:print ((Duration 124.256956)) Duration:string ((Duration 124.256956))
它会大大减慢执行速度。