是什么使Lisp宏如此特别?


297

阅读Paul Graham关于编程语言的文章,人们会认为Lisp宏是唯一的选择。作为繁忙的开发人员,在其他平台上工作,我没有使用Lisp宏的特权。作为想了解蜂鸣声的人,请解释一下此功能如此强大的原因。

还请将此与我从Python,Java,C#或C开发的世界中了解的东西联系起来。


2
顺便说一句,对于C#,有一个LISP风格的宏处理器,名为LeMP:ecsharp.net/lemp ... JavaScript也有一个名为Sweet.js的宏:sweetjs.org
Qwertie

@Qwertie sweetjs现在还能工作吗?
fredrik.hjarner,

我没有使用过,但是最近一次提交是在六个月前的……对我来说足够好了!
Qwertie

Answers:


308

为了给出简短的答案,宏用于定义Common Lisp或领域特定语言(DSL)的语言语法扩展。这些语言直接嵌入到现有的Lisp代码中。现在,DSL可以具有类似于Lisp的语法(例如Peter Norvig的Common Lisp 的Prolog解释器)或完全不同的语法(例如Clojure的Infix Notation Math)。

这是一个更具体的示例:
Python在语言中内置了列表理解功能。这给出了常见情况的简单语法。线

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

产生一个包含0到9之间所有偶数的列表。在Python 1.5天内,还没有这种语法。您将使用更多类似这样的内容:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

这些在功能上都是等效的。让我们调用我们的怀疑暂停,并假设Lisp具有一个非常有限的循环宏,该宏仅进行迭代,并且没有简单的方法来执行等效于列表理解的工作。

在Lisp中,您可以编写以下内容。我应该注意,这个人为设计的示例与Python代码相同,而不是Lisp代码的良好示例。

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

在继续之前,我应该更好地解释什么是宏。这是对代码逐个代码执行的转换。也就是说,由解释器(或编译器)读取的一段代码将代码作为参数,进行处理并返回结果,然后就地运行该结果。

当然,这是很多类型的工作,程序员是懒惰的。因此,我们可以定义DSL来进行列表理解。实际上,我们已经在使用一个宏(循环宏)。

Lisp定义了几种特殊的语法形式。引号(')表示下一个标记是文字。准引号或反引号(`)表示下一个标记是带有转义符的文字。转义由逗号运算符指示。字面'(1 2 3)量等同于Python的[1, 2, 3]。您可以将其分配给另一个变量或就地使用它。您可以认为`(1 2 ,x)它等同于Python[1, 2, x] where x定义为先前的变量。此列表表示法是宏魔术的一部分。第二部分是Lisp阅读器,它可以智能地将宏替换为代码,但是最好在下面进行说明:

因此,我们可以定义一个宏lcomp(列表理解的简称)。它的语法将与示例中使用的python完全相同[x for x in range(10) if x % 2 == 0] -(lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

现在我们可以在命令行执行:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

很整洁吧?现在它不止于此。如果需要,您可以使用一种机制或画笔。您可以具有任何可能需要的语法。像Python或C#的with语法。或.NET的LINQ语法。最后,这就是吸引人们使用Lisp的原因-最终的灵活性。


51
+1用于在Lisp中实现列表理解,因为为什么不呢?
ckb

9
@ckb实际上LISP在标准库中已经有一个列表理解宏:(loop for x from 0 below 10 when (evenp x) collect x)这里有更多示例。但是确实,循环是“只是一个宏”(我实际上是从前从头开始重新实现的
SuzanneDupéron2013年

8
我知道这是完全无关的,但是我想知道语法以及解析实际上是如何工作的...说我这样称呼lcomp(将请求项从“ for”更改为“ azertyuiop”):((lcomp x azertyuiop x in(范围10)如果(=(%x 2)0))宏是否仍将按预期工作?还是在循环中使用“ for”参数,以便在调用时必须为字符串“ for”?
dader

2
@dader因此,循环实际上是另一个宏,for是该宏中使用的特殊符号。我可以很容易地在没有循环宏的情况下定义宏。但是,如果有的话,该职位的时间会更长。循环宏及其所有语法在CLtL中定义。
gte525u 2014年

3
我对其他语言的宏感到困惑的一件事是,它们的宏受宿主语言的语法限制。Lispy宏可以解释非Lispy语法。我的意思是,想象一下创建一个类似haskell的语法(无括号)并使用Lisp宏对其进行解释。这可能吗?与直接使用词法分析器和解析器相比,使用宏的利弊是什么?
CMCDragonkai 2014年

105

您将在此处找到有关Lisp宏的全面辩论

该文章的一个有趣的子集:

在大多数编程语言中,语法很复杂。宏必须分解程序语法,对其进行分析并重新组装。他们没有访问该程序的解析器的权限,因此必须依靠启发式方法和最佳猜测。有时,他们的降价分析是错误的,然后它们崩溃了。

但是Lisp是不同的。Lisp宏确实可以访问解析器,这是一个非常简单的解析器。 Lisp宏不是一个字符串,而是一个准备好的源代码列表,因为Lisp程序的源不是字符串;这是一个列表。Lisp程序确实擅长分解列表并将它们放在一起。他们每天都可靠地做到这一点。

这是一个扩展的示例。Lisp有一个称为“ setf”的宏,可以执行分配。setf最简单的形式是

  (setf x whatever)

将符号“ x”的值设置为表达式“ whatever”的值。

Lisp也有清单。您可以使用“ car”和“ cdr”函数分别获取列表的第一个元素或列表的其余元素。

现在,如果要用新值替换列表的第一个元素怎么办?有一个标准功能可以做到这一点,而且令人难以置信的是,它的名称甚至比“ car”还差。是“ rplaca”。但是您不必记住“ rplaca”,因为您可以编写

  (setf (car somelist) whatever)

设定某人的汽车。

这里真正发生的是“ setf”是一个宏。在编译时,它检查其参数,并发现第一个参数具有形式(car SOMETHING)。它自言自语:“哦,程序员正在尝试设置东西。要使用的功能是'rplaca'。” 并且它悄悄地将代码重写为:

  (rplaca somelist whatever)

5
setf很好地说明了宏的功能,感谢您包括它。
乔尔2010年

我喜欢突出显示..,因为Lisp程序的源文件不是字符串;这是一个列表。!这是LISP宏由于其括号而优于其他大多数宏的主要原因吗?
学生

@学生,我想是这样:books.google.fr/…建议您是对的。
VonC

55

通用Lisp宏实质上扩展了代码的“语法原语”。

例如,在C语言中,switch / case构造仅适用于整数类型,如果要将其用于浮点数或字符串,则将剩下嵌套的if语句和显式比较。您也无法编写C宏来为您完成这项工作。

但是,由于lisp宏(基本上)是一个lisp程序,该程序以代码段作为输入并返回代码以替换宏的“调用”,因此您可以将“原始”指令集扩展到所需的范围,通常以使用更具可读性的程序。

要在C中执行相同的操作,您将必须编写一个自定义的预处理程序,该程序占用您的初始(非相当C的)源,并吐出C编译器可以理解的内容。这不是错误的解决方法,但不一定是最简单的方法。


41

Lisp宏使您可以决定何时(如果有的话)评估任何部分或表达式。举一个简单的例子,考虑一下C:

expr1 && expr2 && expr3 ...

这就是说:评估expr1,如果确实如此,请评估expr2,等等。

现在尝试将其&&变成一个函数...没错,您不能。调用类似:

and(expr1, expr2, expr3)

exprs不管答案是否expr1为假,都会在给出答案之前对这三个方法进行评估!

使用lisp宏,您可以编写类似以下内容的代码:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

现在您有了一个&&,可以像一个函数一样调用它,并且除非它们全部为真,否则它不会评估传递给它的任何形式。

要查看它的用处,请对比一下:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

和:

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

您可以使用宏执行的其他操作包括创建新的关键字和/或迷你语言(查看(loop ...)示例宏),将其他语言集成到Lisp中,例如,您可以编写一个宏,让您说出类似以下内容:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

而且那甚至还没有进入Reader宏

希望这可以帮助。


我认为应该是:“(apply &&,@ exprs);这可能是错误的!”
斯万特·

1
@svante-有两个方面:首先,&&是宏,而不是函数;仅适用于功能。其次,应用传递参数列表,因此您不希望使用“(funcall fn,@ exprs)”,“(应用fn(list,@ exprs)”或“(应用fn,@ exprs nil)”之一,而不是“(应用fn,@ exprs)。”
亚伦,2009年

(and ...将对表达式进行求值,直到一个结果为假,注意由假评估产生的副作用将发生,只有随后的表达式将被跳过。
ocodo


10

想想您可以使用宏和模板在C或C ++中做什么。它们是用于管理重复代码的非常有用的工具,但是它们在相当严格的方面受到限制。

  • 受限的宏/模板语法限制了它们的使用。例如,您不能编写扩展到类或函数以外的模板。宏和模板无法轻松维护内部数据。
  • C和C ++复杂,非常不规则的语法使得很难编写非常通用的宏。

Lisp和Lisp宏解决了这些问题。

  • Lisp宏是用Lisp编写的。您具有Lisp的全部功能来编写宏。
  • Lisp具有非常常规的语法。

与精通C ++的任何人交谈,并询问他们花了多长时间学习模板元编程所需的所有模板知识。或(出色)书中的所有疯狂技巧,例如Modern C ++ Design,尽管该语言已经标准化了十年,但它们仍然很难调试,并且(在实践中)难以在现实世界的编译器之间移植。如果用于元编程的语言与用于编程的语言相同,那么所有这些都将消失!


11
好吧,公平地讲,C ++模板元编程的问题不在于元编程语言不同,而是可怕的-它的设计不如旨在简单得多的模板功能中所发现的那样。
Brooks Moses

@布鲁克斯当然。新兴功能并不总是不好的。不幸的是,以一种缓慢的委员会驱动语言,很难解决它们。真可惜,C ++的现代有用新功能中有很多是用很少有人能读懂的语言编写的,而且普通程序员和“大祭司”之间存在巨大差距。
马特·柯蒂斯

2
@downvoter:如果我的答案有问题,请发表评论,以便我们所有人共享知识。
马特·柯蒂斯

10

lisp宏将程序片段作为输入。该程序片段表示一个数据结构,您可以按照自己喜欢的任何方式对其进行操作和转换。最后,宏输出另一个程序片段,而该片段就是在运行时执行的片段。

C#没有宏功能,但是如果编译器将代码解析为CodeDOM树并将其传递给方法,该方法会将其转换为另一个CodeDOM,然后再编译为IL,则等效。

这可以用来实现“糖”语法,例如for each-statement -clause using,linq -expressions select等,作为转换为基础代码的宏。

如果Java具有宏,则可以在Java中实现Linq语法,而无需Sun更改基本语言。

这是伪代码,说明如何在C#中使用Lisp样式的宏来实现using

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }

现在,这里居然 C#的一个Lisp风格的宏处理器,我想指出,对于宏using这个样子);
Qwertie

9

由于现有的答案给出了很好的具体示例,解释了宏的实现方式以及宏的实现方式,因此,也许有必要将一些有关为什么宏工具相对于其他语言而言具有重要意义的想法收集在一起。首先是这些答案,然后是其他地方的出色答案:

...在C语言中,您将必须编写一个自定义的预处理程序[它可能符合 足够复杂的C程序的要求 ] ...

瓦汀

与精通C ++的任何人交谈,并询问他们花了多长时间学习模板元编程所需的所有模板功能[仍然没有那么强大]。

马特·柯蒂斯Matt Curtis)

...在Java中,您必须通过字节码编织来破解自己的方式,尽管诸如AspectJ之类的某些框架允许您使用不同的方法来做到这一点,但从根本上讲,这是黑客。

- 米格尔·平

DOLIST与Perl的foreach或Python的相似。Java作为JSR-201的一部分,在Java 1.5中用“增强”的for循环添加了类似的循环构造。注意宏的作用。注意到代码中有通用模式的Lisp程序员可以编写宏以为自己提供该模式的源代码级抽象。注意到相同模式的Java程序员必须说服Sun,这种特殊的抽象值得添加到该语言中。然后,Sun必须发布JSR并召集一个行业范围内的“专家组”,以将所有事情都排除在外。根据Sun的说法,该过程平均需要18个月。之后,编译器编写者都必须升级其编译器以支持新功能。即使Java程序员最喜欢的编译器支持新版本的Java,他们可能“仍然”不能使用新功能,直到被允许破坏与旧版本Java的源兼容性。令人讨厌的是,Common Lisp程序员可以在五分钟内自行解决,困扰了Java程序员多年。

- 彼得·塞贝尔Peter Seibel),在“实用普通口齿不清”中


8

我不确定我可以在每个人的(出色)帖子中添加一些见解,但是...

Lisp宏由于Lisp语法的性质而非常有用。

Lisp是一种非常有规律的语言(认为所有事情都是清单);宏使您可以将数据和代码视为相同(不需要字符串解析或其他修改来修改Lisp表达式)。您将这两个功能结合在一起,就拥有了一种非常干净的修改代码的方法。

编辑:我想说的是Lisp是homoiconic,这意味着lisp程序的数据结构是用lisp本身编写的。

因此,最终您将获得一种在语言之上使用语言本身的全部功能创建自己的代码生成器的方式(例如,在Java中,您必须通过字节码编织来破解自己的方式,尽管某些框架(例如AspectJ)允许您使用不同的方法来执行此操作,从根本上说,这是一个技巧。

在实践中,使用宏最终可以在Lisp之上构建自己的迷你语言,而无需学习其他语言或工具,并且可以充分利用语言本身的强大功能。


1
这是有见地的评论,但是,“一切都是清单”的想法可能会吓到新来者。要了解列表,您需要了解缺点,汽车,cdrs,单元格。更准确地说,Lisp由组成S-expressions,而不是列表。
Ribamar '18

6

Lisp宏表示几乎在任何大型编程项目中都会出现的模式。最终,在一个大型程序中,您将具有一定的代码段,您会意识到编写一个程序将源代码输出为文本,然后将其粘贴即可,这样会更简单且更不会出错。

在Python中,对象有__repr__和和两种方法__str____str__仅仅是人类可读的表示。 __repr__返回有效的Python代码的表示形式,也就是说,可以将其作为有效的Python输入到解释器中。这样,您可以创建一些小的Python片段,以生成可粘贴到实际源代码中的有效代码。

在Lisp中,整个过程已由宏系统形式化。当然,它使您能够创建语法的扩展并完成各种奇特的事情,但是上面总结了它的实际用途。当然,Lisp宏系统允许您使用整个语言的全部功能来操纵这些“片段”,这很有帮助。


1
Lisp局外人非常清楚您的第一段,这一点很重要。
2015年

5

简而言之,宏是代码的转换。它们允许引入许多新的语法构造。例如,在C#中考虑LINQ。在Lisp中,存在由宏实现的类似语言扩展(例如,内置循环构造,迭代)。宏大大减少了代码重复。宏允许嵌入“小语言”(例如,在c#/ java中,人们将使用xml进行配置,而在lisp中,宏可以实现相同的功能)。宏可能隐藏使用库用法的困难。

例如,您可以轻而易举地写下

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

并且这隐藏了所有数据库内容(事务,正确的连接关闭,获取数据等),而在C#中,这需要创建SqlConnections,SqlCommands,将SqlParameters添加到SqlCommands,在SqlDataReaders上循环并正确关闭它们。


3

尽管以上全部解释了什么是宏,甚至还有很酷的示例,但我认为宏和普通函数之间的主要区别在于LISP在调用函数之前首先评估所有参数。与宏相反,LISP将未评估的参数传递给宏。例如,如果将(+ 1 2)传递给一个函数,则该函数将接收值3。如果将该值传递给一个宏,则它将接收一个List(+ 1 2)。这可以用来做各种难以置信的有用的事情。

  • 添加新的控制结构,例如循环或解构列表
  • 测量执行传入的函数所花费的时间。对于使用函数的参数,将在将控制权传递给函数之前先评估参数。使用宏,您可以在秒表的开始和停止之间拼接代码。以下代码在宏和函数中具有完全相同的代码,并且输出非常不同。注意:这是一个人为设计的示例,因此选择了实现方式,以便使其完全相同以更好地突出显示差异。

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0

顺便说一句,Scala已为该语言添加了宏。尽管它们缺乏Lisp宏的美,因为该语言不是同质的,但绝对值得对其进行研究,而且它们提供的抽象语法树最终可能更易于使用。现在说我喜欢哪种宏系统还为时过早。
Joerg Schmuecker

2
“ LISP将未经评估的参数传递给了宏”,最后一个回答很明确地说。但是您忘了它的后半部分:宏的结果是一个转换后的代码,系统将代替原始代码对代码进行整体评估,就像原始代码一样(除非它本身再次是对宏,这次也将通过宏进行转换)。
Will Ness

0

我从《常见的lisp食谱》中获得了此信息,但我认为它解释了为什么lisp宏以一种不错的方式很好。

“宏是一段普通的Lisp代码,可在另一段假定的Lisp代码上运行,将其转换为(更接近于)可执行Lisp的版本。这听起来可能有些复杂,所以让我们举一个简单的例子。假设您想要一个将两个变量设置为相同值的setq版本。因此,如果您编写

(setq2 x y (+ z 3))

z=8x和y都设置为11时(我想不出什么用,但这只是一个例子。)

很明显,我们不能将setq2定义为一个函数。如果x=50y=-5,则此函数将接收值50,-5和11;它不会知道应该设置哪些变量。我们真正要说的是,当您(Lisp系统)看到时(setq2 v1 v2 e),将其等同于(progn (setq v1 e) (setq v2 e))。实际上,这不是很正确,但是现在可以。宏允许我们通过指定用于将输入模式(setq2 v1 v2 e)“转换为输出模式(progn ...)” 的程序来精确地做到这一点。

如果您认为这很好,则可以继续阅读以下内容:http : //cl-cookbook.sourceforge.net/macros.html


1
setq2如果xy通过引用传递,则可以将其定义为一个函数。我不知道在CL中是否可行。因此,对于不特别了解Lisps或CL的人来说,这不是IMO的说明性示例
neoascetic

@neoascetic CL参数传递仅按值传递(这就是为什么它首先需要宏的原因)。虽然有些值是指针(如列表)。
Will Ness

-5

在python中,您具有装饰器,基本上,您有一个将另一个函数作为输入的函数。您可以做任何想做的事情:调用函数,做其他事情,将函数调用包装在资源获取版本中,等等。但是您不必窥视该函数。假设我们想使其功能更强大,比如说您的装饰器以列表的形式接收了该函数的代码,那么您不仅可以按原样执行该函数,还可以现在执行它的一部分,对函数的行进行重新排序等。

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.