那么,LISP如何使宏系统的实现变得更容易呢?


21

我正在从SICP学习Scheme,给人的印象是,使Scheme以及LISP特别与众不同的很大一部分是宏系统。但是,由于宏是在编译时扩展的,所以人们为什么不为C / Python / Java /任何东西制作等效的宏系统?例如,可以将python命令绑定到expand-macros | python或任何其他内容。该代码对于不使用宏系统的人仍然可以移植,只是在发布代码之前扩展宏即可。但是除了C ++ / Haskell中的模板之外,我不知道其他类似的东西,我收集的模板并不完全相同。那么,LISP又如何使实现宏系统更容易呢?


3
“这些代码仍然可以移植给不使用宏系统的人,人们只需在发布代码之前扩展宏即可。” -只是警告您,这往往效果不佳。那些其他人也可以运行代码,但是在实践中,宏扩展代码通常难以理解,并且通常难以修改。从作者没有为人眼量身定制扩展的代码,而是为真正的源代码量身定制的意义上来说,这实际上是“糟糕的编写”。尝试告诉Java程序员您通过C预处理器运行Java代码,并观察它们变成什么颜色;-)
史蒂夫·杰索普

1
但是,宏需要执行,这时您已经在为该语言编写解释器。
Mehrdad 2015年

Answers:


29

许多Lispers会告诉您,使Lisp与众不同的地方是homoiconicity,这意味着该代码的语法使用与其他数据相同的数据结构表示。例如,这是一个简单的函数(使用Scheme语法),用于计算具有给定边长的直角三角形的斜边:

(define (hypot x y)
  (sqrt (+ (square x) (square y))))

现在,谐音说上面的代码实际上可以用Lisp代码表示为数据结构(特别是列表列表)。因此,请考虑以下列表,并查看它们如何“粘合”在一起:

  1. (define #2# #3#)
  2. (hypot x y)
  3. (sqrt #4#)
  4. (+ #5# #6#)
  5. (square x)
  6. (square y)

宏使您可以将源代码视为:清单。每个那些6“子列表”的任一含有指向其他列表,或以符号(在本例中:definehypotxysqrt+square)。


那么,我们如何才能使用谐音来“分离”语法并生成宏呢?这是一个简单的例子。让我们重新实现let我们称为的宏my-let。提醒一句,

(my-let ((foo 1)
         (bar 2))
  (+ foo bar))

应该扩展为

((lambda (foo bar)
   (+ foo bar))
 1 2)

这是使用Scheme“显式重命名”宏†的实现

(define-syntax my-let
  (er-macro-transformer
    (lambda (form rename compare)
      (define bindings (cadr form))
      (define body (cddr form))
      `((,(rename 'lambda) ,(map car bindings)
          ,@body)
        ,@(map cadr bindings)))))

form参数绑定到实际的形式,所以在我们的例子,这将是(my-let ((foo 1) (bar 2)) (+ foo bar))。因此,让我们看一下示例:

  1. 首先,我们从表单中检索绑定。cadr抓取((foo 1) (bar 2))表单的一部分。
  2. 然后,我们从表单中检索主体。cddr抓取((+ foo bar))表单的一部分。(请注意,这是为了在绑定后获取所有子表单;因此,如果表单是

    (my-let ((foo 1)
             (bar 2))
      (debug foo)
      (debug bar)
      (+ foo bar))
    

    那么身体会是((debug foo) (debug bar) (+ foo bar))。)

  3. 现在,我们实际上构建了结果lambda表达式,并使用已收集的绑定和主体进行调用。反引号称为“准引用”,表示将准引用内的所有内容都视为文字数据,除了逗号后的位(“ 取消引用”)。
    • 定义此宏时有效(rename 'lambda)使用lambda绑定的方法,而不是使用此宏时可能发生的绑定。(这被称为卫生。)lambda
    • (map car bindings)返回(foo bar):每个绑定中的第一个基准。
    • (map cadr bindings)返回(1 2):每个绑定中的第二个数据。
    • ,@ 执行“拼接”,用于返回列表的表达式:它导致将列表的元素粘贴到结果中,而不是列表本身。
  4. 总而言之,我们得到列表(($lambda (foo bar) (+ foo bar)) 1 2)$lambda这里是指已重命名的lambda

坦白吧?;-)(如果这对您来说不那么简单,请想象一下为其他语言实现宏系统会有多么困难。)


因此,如果您能够以一种非笨拙的方式“分拆”源代码,则可以拥有用于其他语言的宏系统。有一些尝试。例如,sweet.js针对JavaScript执行此操作。

†对于有经验的Schemer,我有意选择使用显式重命名宏作为defmacro其他Lisp方言所使用s 之间的中间折衷,syntax-rules(这将是在Scheme中实现此类宏的标准方法)。我不想写其他Lisp方言,但我不想疏远不熟悉的非Schemers syntax-rules

供参考,以下my-let是使用的宏syntax-rules

(define-syntax my-let
  (syntax-rules ()
    ((my-let ((id val) ...)
       body ...)
     ((lambda (id ...)
        body ...)
      val ...))))

相应的syntax-case版本看起来非常相似:

(define-syntax my-let
  (lambda (stx)
    (syntax-case stx ()
      ((_ ((id val) ...)
         body ...)
       #'((lambda (id ...)
            body ...)
          val ...)))))

两者之间的区别在于in 中的所有内容都应用syntax-rules了隐式#',因此您只能在中使用模式/模板对syntax-rules,因此它是完全声明性的。相反,在中syntax-case,模式后面的位是实际代码,最后必须返回语法对象(#'(...)),但也可以包含其他代码。


2
您没有提到的一个优点:是的,还有其他语言的尝试,例如用于JS的sweet.js。但是,在lisps中,编写宏的方式与编写函数的语言相同。
Florian Margaine 2015年

是的,您可以使用Lisp语言编写过程性(相对于声明性)宏,这使您可以做真正的高级工作。顺便说一句,这就是我对Scheme宏系统的满意之处:有多种选择。对于简单的宏,我使用syntax-rules纯粹是声明性的。对于复杂的宏,我可以使用syntax-case,它部分是声明性的,部分是过程性的。然后是明确的重命名,这纯粹是程序上的。(大多数Scheme实现将提供syntax-caseER或两者都提供。我还没有看到同时提供ER的功能。它们的功能相当。)
Chris Jester-Young

为什么宏必须修改AST?他们为什么不能在更高层次上工作?
Elliot Gorokhovsky

1
那么为什么LISP更好呢?LISP为何与众不同?如果一个人可以用js实现宏,那么肯定也可以用任何其他语言来实现它们。
Elliot Gorokhovsky

3
正如我在第一条评论中所说的那样,@RenéG的一大优势是您仍在使用相同的语言进行写作。
Florian Margaine 2015年

23

持不同意见:Lisp的同质性远不如大多数Lisp粉丝所想的有用。

要了解语法宏,了解编译器很重要。编译器的任务是将人类可读的代码转换为可执行代码。从非常高级的角度来看,这有两个总体阶段:解析代码生成

解析是读取代码,根据一组正式规则对其进行解释并将其转换为树结构(通常称为AST(抽象语法树))的过程。对于编程语言之间的所有多样性,这是一个了不起的共性:基本上,每种通用编程语言都解析为树形结构。

代码生成将解析器的AST作为输入,并通过应用形式规则将其转换为可执行代码。从性能的角度来看,这是一个简单得多的任务。许多高级语言编译器将其时间的75%或以上花费在解析上。

关于Lisp,要记住的是,它非常古老。在编程语言中,只有FORTRAN早于Lisp。追溯到今天,解析(编译的缓慢部分)被认为是一种黑暗而神秘的艺术。约翰·麦卡锡(John McCarthy)的有关Lisp理论的原始论文(当时那只是一个他从未想过可以真正实现为真正的计算机编程语言的想法)描述了比现代的“无处不在的S表达式”更复杂和更具表现力的语法。 ”符号。那是后来人们尝试实际实施的。由于当时的解析还不为人所理解,因此他们对其进行了深入研究,并将语法简化为谐音树状结构,以使解析器的工作变得非常琐碎。最终结果是您(开发人员)必须做很多解析器。通过将正式的AST直接编写到您的代码中来完成此工作。同质性并没有“使宏变得那么容易”,而是使编写其他所有内容变得更加困难!

问题是,特别是对于动态类型,S表达式很难携带大量语义信息。当所有语法都是同一种事物(列表列表)时,语法提供的上下文方式就没有太多了,因此宏系统几乎无法使用。

自1960年代Lisp发明以来,编译器理论已经走了很长一段路,尽管它的成就在当时是令人印象深刻的,但它们现在看起来相当原始。对于现代元编程系统的示例,请看一下(严重不被重视的)Boo语言。Boo是静态类型的,面向对象的并且是开源的,因此每个AST节点都具有一种类型明确的结构,宏开发人员可以将其读取代码。该语言具有受Python启发的相对简单的语法,并具有各种关键字,这些关键字赋予从其构建的树结构固有的语义含义,并且其元编程具有直观的准引用语法,可简化新AST节点的创建。

这是昨天创建的宏,当我意识到我将相同的模式应用于GUI代码中的许多不同位置时,我将BeginUpdate()在这些位置调用UI控件,在一个try块中执行更新,然后调用EndUpdate()

macro UIUpdate(value as Expression):
    return [|
        $value.BeginUpdate()
        try:
            $(UIUpdate.Body)
        ensure:
            $value.EndUpdate()
    |]

macro实际上,该命令本身就是一个宏,它以宏主体作为输入并生成一个类来处理宏。它使用宏的名称作为MacroStatement代表表示宏调用的AST节点的变量。[| ... |]是一个准报价块,生成与该报价之内和之内的代码相对应的AST,$符号提供“取消报价”功能,替换为指定的节点。

这样,可以写:

UIUpdate myComboBox:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0

并将其扩展为:

myComboBox.BeginUpdate()
try:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0
ensure:
   myComboBox.EndUpdate()

与Lisp宏相比,用这种方式表达宏更简单,更直观,因为开发人员知道的结构MacroStatement并知道ArgumentsBody属性如何工作,并且固有的知识可以用来表达非常直观的概念。道路。这也更安全,因为编译器知道的结构MacroStatement,并且如果您尝试编写对a无效的内容MacroStatement,则编译器会立即捕获到该错误并报告错误,而不是您不知道,直到出现问题运行。

将宏移植到Haskell,Python,Java,Scala等上并不困难,因为这些语言不是谐音。这是很困难的,因为这些语言不是为它们设计的,并且当您的语言的AST层次结构是从头开始设计并由宏系统进行检查和操作时,它的效果最佳。当您从一开始就使用为元编程而设计的语言时,宏就更容易使用!


4
欢乐阅读,谢谢!非Lisp宏会扩展到语法更改的程度吗?由于Lisp的优势之一就是语法完全相同,因此很容易添加函数,条件语句,无论它们是相同的。使用非Lisp语言时,一件事与另一件事有所不同- if...例如,看起来并不像函数调用。我不了解Boo,但可以想象Boo没有模式匹配,您能以它自己的语法作为宏来介绍它吗?我的意思是-Lisp中的任何新宏都会感觉100%自然,使用其他语言也可以,但是您可以看到针迹。
greenoldman

4
我一直读的故事有点不同。计划使用s表达式的另一种语法,但由于程序员已经开始使用s表达式并发现它们很方便,因此延迟了工作。因此,有关新语法的工作最终被遗忘了。您能否引用表明编译器理论的缺点的来源作为使用s表达式的原因?此外,Lisp家族持续发展了数十年(Scheme,Common Lisp,Clojure),大多数方言决定坚持使用S表达式。
Giorgio

5
“更简单,更直观”:对不起,但我不知道如何。“ Updating.Arguments [0]” 没有意义,我宁愿有一个命名的参数,并让编译器检查参数数量是否匹配的自身:pastebin.com/YtUf1FpG
coredump

8
“从性能的角度来看,这是一个简单得多的任务;许多高级语言编译器将其时间的75%或更多花费在解析上。” 我本来希望在大多数时间里寻找并应用优化方法(但是我从未编写过真正的编译器)。我在这里想念什么吗?
2015年

5
不幸的是,您的示例并未显示出这一点。它是在任何具有宏的Lisp中实现的原始方法。实际上,这是要实现的最原始的宏之一。这使我怀疑您对Lisp中的宏知之甚少。“ Lisp的语法停留在1960年代”:实际上,Lisp的宏系统自1960年以来就取得了很大的进步(1960年Lisp甚至没有宏!)。
Rainer Joswig 2015年

3

我正在从SICP学习Scheme,给人的印象是,使Scheme以及LISP与众不同的很大一部分是宏系统。

为何如此?SICP中的所有代码均以无宏样式编写。SICP中没有宏。仅在第373页的脚注中提到过宏。

但是,由于宏是在编译时扩展的

他们不一定。Lisp在解释器和编译器中都提供了宏。因此,可能没有编译时间。如果您有Lisp解释器,则宏将在执行时扩展。由于许多Lisp系统都带有编译器,因此可以生成代码并在运行时进行编译。

让我们使用Common Lisp实现SBCL进行测试。

让我们将SBCL切换到解释器:

* (setf sb-ext:*evaluator-mode* :interpret)

:INTERPRET

现在我们定义一个宏。调用代码扩展时,宏会打印出一些内容。生成的代码不打印。

* (defmacro my-and (a b)
    (print "macro my-and used")
    `(if ,a
         (if ,b t nil)
         nil))

现在让我们使用宏:

MY-AND
* (defun foo (a b) (my-and a b))

FOO

看到。在上述情况下,Lisp不会执行任何操作。宏在定义时不会扩展。

* (foo t nil)

"macro my-and used"
NIL

但是在运行时,使用代码时,宏会被扩展。

* (foo t t)

"macro my-and used"
T

同样,在运行时,使用代码时,宏将被扩展。

请注意,使用编译器时,SBCL仅会扩展一次。但是,各种Lisp实现也提供了解释器,例如SBCL。

为什么在Lisp中宏很容易?好吧,它们并不是很容易。仅在Lisps中,并且有许多内置了宏支持。由于许多Lisps都带有用于宏的广泛机制,因此看起来很简单。但是宏机制可能非常复杂。


我已经在网上阅读了很多有关Scheme的内容,也阅读了SICP。另外,在解释Lisp表达式之前,是否不对其进行编译?它们至少必须被解析。所以我猜“编译时间”应该是“解析时间”。
Elliot Gorokhovsky

我相信,@RenéGRainer的观点是,如果您evalload使用任何Lisp语言进行编码,这些语言中的宏也将得到处理。而如果您使用问题中建议的预处理器系统,eval则类似的方法将无法从宏扩展中受益。
克里斯·杰斯特·杨

@RenéG另外,read在Lisp中也称为“ parse” 。这种区别很重要,因为它eval适用于实际的列表数据结构(如我的回答所述),而不适用于文本形式。因此,您可以使用(eval '(+ 1 1))并返回2,但如果返回,则(eval "(+ 1 1)")返回"(+ 1 1)"(字符串)。您通常使用read"(+ 1 1)"(一个7个字符的字符串)到(+ 1 1)(一个带有一个符号和两个fixnum的列表)获取。
克里斯·杰斯特·杨

@RenéG有了这样的理解,宏在read-time时不起作用。它们在编译时起作用,这意味着,如果您具有像这样的代码,则在加载代码时(而不是在每次运行代码时)(and (test1) (test2))它将一次扩展为(if (test1) (test2) #f)(在Scheme中),但是如果您执行类似的操作,(eval '(and (test1) (test2))),它将在运行时适当地编译(和宏扩展)该表达式。
克里斯·杰斯特·杨

@RenéGHomoiconicity允许Lisp语言在列表结构而不是文本形式上进行评估,并在执行之前对这些列表结构进行转换(通过宏)。大多数语言eval仅在文本字符串上起作用,并且它们的语法修改功能更加乏味和/或繁琐。
克里斯·杰斯特·杨

1

同质性使实现宏变得容易得多。代码是数据而数据是代码的想法使得(或多或少地)防止标识符的意外捕获(由卫生宏解决)成为可能,从而可以自由地用一个替换另一个。Lisp和Scheme通过S结构统一的S表达式的语法使此操作变得更容易,从而易于转换为构成语法宏基础的AST 。

尽管仍然可以实现,但没有S表达式或同音性的语言在实现语法宏时会遇到麻烦。开普勒计划正试图将它们引入Scala。

除了非同质性之外,语法宏用法的最大问题是任意生成的语法问题。它们提供了极大的灵活性和功能,但是代价是您的源代码可能不再那么容易理解或维护。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.