我正在尝试编写一组Scheme宏来简化使用声明式风格构建简单的图形结构,但作为一个初学者Schemer,我正在努力解决宏卫生问题。
我定义了两种记录类型
:graph
和:node
:
(define-record-type :graph
(make-graph nodes links)
graph?
(nodes graph-nodes)
(links node-links))
(define-record-type :node
(make-node name pins)
node?
(name node-name)
(pins node-pins))
在我的示例中,pin
只是一个字符串(引脚名称),link
只是endpoint
的缺点对,而endpoint
则是节点名称和引脚名称的缺点对。
我希望能够做的是定义宏的
graph
、node
和 link
,允许我以声明式风格创建图形,如下所示:
(define g (graph
(node "v0"
(pin "out"))
(node "v1"
(pin "out"))
(node "add"
(pin "lhs")
(pin "rhs")
(pin "out"))
(link (("v0" . "out") . ("add" . "lhs"))
(link (("v1" . "out") . ("add" . "rhs"))))
此表达式的结果将是
:graph
类型的值,其中 (graph-nodes g)
将计算为 3 个 :node
值的列表,(graph-links g)
将计算为 ((("v0"."out") . "add" . "lhs")) (("v1" . "out") . ("add" . "rhs")))
,并且 3 个节点中的每一个都将是:node
值与定义它们的 pins
。
一个额外的要求是,只有
graph
宏才能在全局范围内定义,我非常希望避免为我可以作为 body
调用的 graph
传递的任何内容定义全局宏。这些宏仅在 graph
宏扩展的上下文中才有意义,我想为它们维护“尽可能小的范围”原则,并且也不用像 node
这样的通用名称来乱扔全局范围或 pin
。
显然,直接使用
make-graph
创建图形并将节点和链接直接作为列表传递会相对简单。不过,我更喜欢使用宏,因为它允许使用更具声明性的样式来定义图形:您不需要记住要传入的参数的顺序,您可以混合节点/链接声明的顺序,扩展图形在不破坏图形定义代码的情况下记录更多附加字段,轻松地将宏调用转换为其他数据类型(XML、JSN)等。
我尝试使用嵌套宏来解决这个问题:全局
graph
宏,它有一个本地 node
宏。
为了简单起见,我将在此示例中省略定义链接或引脚,以扩展此示例,
let-syntax
将有一个附加宏 link
,并且 node
的宏本身将有另一个名为的嵌套宏pin
(所以 let-syntax
代表 pin
内部 let-syntax
代表 node
内部 define-syntax
代表 graph
):
(define-syntax graph
(syntax-rules ()
((graph . body)
(let ((nodes '())
(links '()))
(let-syntax ((node
(syntax-rules ()
((node name))
(make-node name '()))))
(begin . body))
(make-graph nodes links)))))
(graph (node "v0") (node "v1"))
不幸的是,这不起作用,因为在节点扩展中插入
node
时,body
将被解除绑定。我想我明白为什么:由于Scheme宏是卫生的,任何变量(模式变量或局部变量)都将被扩展/重命名为宏局部变量,因此由node
定义的let-syntax
宏实际上不会被称为node
扩展后不再存在,因此我传递到 node
宏中的 body
中的 graph
将看不到它。
这里提出了一个非常相似的问题,虽然我有点理解那里的解释,但我不确定如何解释建议的解决方案/解决方法。
如果我没读错的话,另一个问题中的两个解决方案都涉及a)显式传递/匹配对我想从传递到body
中调用的任何函数的引用(所以在我的例子中,
graph
,
node
、
link
),或 b) 将
pin
、
node
、
link
定义为全局宏。
解决方案 a) 没有吸引力,因为它违背了宏的目的(即以声明式风格定义图形,使用最少的语法),而解决方案 b) 感觉它违背了卫生宏的全部意义,因为现在我需要在全局范围内使用像
pin
、node
和
link
这样的宏,它们很可能与想要定义同名事物(局部变量、符号)的代码发生冲突。
考虑到人们已经能够使用Scheme宏创建DSL来完成最疯狂的事情,我几乎可以肯定我想做的事情应该可以通过某种方式实现。我还没有弄清楚,这让我觉得我错过了谜题的一些基本部分和/或推理自己进入了方案(嵌套/递归宏)的某个黑暗角落,我不应该参与其中因为有更简单的解决方案可以实现我想要的。
我不确定你是否可以用
syntax-rules
来验证参数的结构和类型宏,在单个宏中就变得相当容易。
注意我更改了
syntax-rules
子句的语法
link
(link ((from-node . from-pin) . (to-node . to-pin)))
; Same as (link ((from-node . from-pin) to-node . to-pin))
更简单、更容易使用。 2 元素对列表的列表也存储在生成的图形记录中。您还可以考虑对所有这些标识符使用符号而不是字符串,这在 lisp 家族语言中更为惯用。
(link (from-node . from-pin) (to-node . to-pin))