对于使用一流函数无法做到的Lisp宏,你能做些什么?

Jos*_*Fox 28 lisp python macros

我想我理解Lisp宏及其在编译阶段的作用.

但是在Python中,您可以将函数传递给另一个函数

def f(filename, g):
  try:                                
     fh = open(filename, "rb") 
     g(fh)
  finally:
     close(fh) 
Run Code Online (Sandbox Code Playgroud)

所以,我们在这里得到懒惰的评价.我可以用宏做什么而不用函数作为第一类对象?

sep*_*p2k 28

首先,Lisp也有一流的功能,所以你也可以问:"如果我已经拥有一流的功能,为什么我需要在Lisp中使用宏".答案是,第一类函数不允许您使用语法.

在化妆品的水平,一流的功能,让你写f(filename, some_function)f(filename, lambda fh: fh.whatever(x)),但不是f(filename, fh, fh.whatever(x)).虽然可以说这是一件好事,因为在最后一个案例中fh突然来自的地方不太清楚.

更重要的是,函数只能包含有效的代码.所以你不能编写一个高阶函数reverse_function,它将一个函数作为一个参数并"反向"执行它,这样reverse_function(lambda: "hello world" print)才能执行print "hello world".使用宏,您可以执行此操作.当然这个特定的例子非常愚蠢,但是这种能力在嵌入领域特定语言时非常有用.

例如,您无法loop在python中实现常见的lisp 构造.地狱,你甚至不能for ... in在python中实现python的构造,如果它不是真正的内置 - 至少不是那种语法.当然你可以实现类似的东西for(collection, function),但这不是那么漂亮.

  • 为了抽象并希望澄清一点,Lisp宏允许您控制是否(以及何时)参数被评估; 功能没有.使用函数时,始终在调用函数时计算参数.使用宏,参数作为普通数据传递给宏.然后宏代码决定它们是否以及何时应该被评估.这就是为什么你不能在没有宏的情况下编写reverse_function的原因:作为一个函数,在函数体有机会重写之前,会对参数进行求值(并导致错误). (8认同)
  • 谢谢你的回答.至于前两段:你能想到给出一些非愚蠢的,但仍然可以理解的例子吗? (3认同)
  • @Joshua:DSL,例如用于嵌入SQL(不在代码中的字符串中),是一种常见的用例. (2认同)
  • `CASE`怎么样?Common Lisp有一个CASE宏(实际上是其中一些),我发现它非常有用. (2认同)

jac*_*obm 12

这是2002年Matthias Felleisen的回答(来自http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg01539.html):

我想提出宏的三个用途:

  1. 数据子语言:我可以编写简单的表达式,并创建复杂的嵌套列表/数组/表,其中包含引号,unquote等,整齐地装饰着宏.

  2. 绑定结构:我可以用宏引入新的绑定结构.这有助于我摆脱lambda并将更紧密的东西放在一起.例如,我们的一个教学包中包含一个表单
    (web-query([姓氏(字符串 - 附加"Hello"名字"你的姓氏是什么?"))...姓氏...名字...)与程序和Web消费者隐含的明显交互.
    [注意:在ML中你可以编写web-query(fn last-name => ...)string_append(...)但是通过golly这是一个痛苦和不必要的模式.]

  3. 评估重新排序:我可以根据需要引入延迟/推迟表达式评估的结构.想想循环,新条件,延迟/强制等等.
    [注意:在Haskell中,你不需要那个.]

我理解Lispers出于其他原因使用宏.老实说,我认为这部分是由于编译器的缺陷,部分是由于目标语言中的"语义"不规范.

当他们说X语言可以做宏可以做什么时,我挑战人们解决所有三个问题.

- 马蒂亚斯

Felleisen是该领域最具影响力的宏观研究人员之一.(不过我不知道他是否会同意这个消息.)

更多阅读:Paul Graham的On Lisp(http://www.paulgraham.com/onlisp.html ; Graham 绝对不同意Felleisen这些是宏的唯一有用用途),以及Shriram Krishnamurthi的论文"Automata via Macros" (http://www.cs.brown.edu/~sk/Publications/Papers/Published/sk-automata-macros/).


SK-*_*gic 8

宏在编译时扩展.闭包是在运行时构造的.使用宏,您可以实现嵌入式域特定语言的高效编译器,并且通过高阶函数,您只能实现低效的解释器.eDSL编译器可以进行各种静态检查,执行您想要实现的任何昂贵的优化,但是当您只有运行时,您就无法做任何昂贵的事情.

不用说,宏为您的eDSL和语言扩展提供了更灵活的语法(字面意思,任何语法).

有关更多详细信息,请参阅此问题的答案:使用宏收集Great Applications和程序


Rai*_*wig 8

宏进行代码转换

宏转换源代码.懒惰的评价没有.想象一下,您现在可以编写将任意代码转换为任意不同代码的函数.

非常简单的代码转换

简单语言结构的创建也只是一个非常简单的例子.考虑打开文件的示例:

(with-open-file (stream file :direction :input)
  (do-something stream))
Run Code Online (Sandbox Code Playgroud)

(call-with-stream (function do-something)
                  file
                  :direction :input)
Run Code Online (Sandbox Code Playgroud)

宏给我的是一种略有不同的语法和代码结构.

嵌入式语言:高级迭代结构

接下来考虑一个稍微不同的例子

(loop for i from 10 below 20 collect (sqr i))
Run Code Online (Sandbox Code Playgroud)

(collect-for 10 20 (function sqr))
Run Code Online (Sandbox Code Playgroud)

我们可以定义一个函数COLLECT-FOR,它对一个简单的循环执行相同的操作,并具有start,end和step函数的变量.

LOOP提供了一种新语言.该LOOP宏对于这种语言的编译器.此编译器可以执行LOOP特定的优化,还可以在编译时检查此新语言的语法.一个更强大的循环宏是ITERATE.现在,语言级别的这些强大工具可以编写为库,而无需任何特殊的编译器支持.

在宏中遍历代码树并进行更改

下一个简单的例子:

(with-slots (age name) some-person
  (print name)
  (princ " "
  (princ age))
Run Code Online (Sandbox Code Playgroud)

与类似的东西:

(flet ((age (person) (slot-value person 'age))
       (name (person) (slot-value person 'name)))
   (print (name))
   (princ " ")
   (princ (age)))
Run Code Online (Sandbox Code Playgroud)

WITH-SLOTS宏导致的封闭源代码树的完整的步行路程,通过调用替换变量名(SLOT-VALUE SOME-PERSON 'name):

(progn
  (print (slot-value some-person 'name))
  (princ " "
  (princ (slot-value some-person 'age)))
Run Code Online (Sandbox Code Playgroud)

在这种情况下,宏可以重写代码的选定部分.它理解Lisp语言的结构,并且知道名称和年龄是变量.它也理解在某些情况下name,age可能不是变量,不应该重写.这是一个所谓的Code Walker的应用程序,这是一种可以遍历代码树并对代码树进行更改的工具.

宏可以修改编译时环境

另一个简单的例子,一个小文件的内容:

(defmacro oneplus (x)
  (print (list 'expanding 'oneplus 'with x))
  `(1+ ,x))

(defun example (a b)
   (+ (oneplus a) (oneplus (* a b))))
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我们对宏不感兴趣ONEPLUS,但在宏DEFMACRO本身.

有趣的是什么?在Lisp中,您可以拥有一个包含上述内容的文件,并使用文件编译器来编译该文件.

;;; Compiling file /private/tmp/test.lisp ...
;;; Safety = 3, Speed = 1, Space = 1, Float = 1, Interruptible = 1
;;; Compilation speed = 1, Debug = 2, Fixnum safety = 3
;;; Source level debugging is on
;;; Source file recording is  on
;;; Cross referencing is on
; (TOP-LEVEL-FORM 0)
; ONEPLUS

(EXPANDING ONEPLUS SOURCE A) 
(EXPANDING ONEPLUS SOURCE (* A B)) 
; EXAMPLE
;; Processing Cross Reference Information
Run Code Online (Sandbox Code Playgroud)

所以我们看到,文件编译器扩展了ONEPLUS宏的使用.

有什么特别之处?文件中有一个宏定义,在下一个表单中我们已经使用了这个新宏ONEPLUS.我们从未将宏定义加载到Lisp中.不知何故,编译器知道并注册定义的宏ONEPLUS,然后才能使用它.

因此,宏在编译时环境中DEFMACRO注册新定义的宏ONEPLUS,以便编译器知道此宏 - 无需加载代码.然后宏可以在宏扩展期间在编译时执行.

有了功能,我们不能这样做.编译器为函数调用创建代码,但不运行它们.但是宏可以在编译时运行并向编译器添加"知识".然后,这些知识在编译器运行期间有效,稍后会被遗忘.DEFMACRO是一个在编译时执行的宏,然后通知编译时环境的新宏.

另请注意,宏ONEPLUS也会运行两次,因为它在文件中使用了两次.副作用是它打印出一些东西.但ONEPLUS也可能有其他任意的副作用.例如,它可以针对规则库检查封闭的源,并提醒您是否例如所附的代码违反了某些规则(考虑样式检查器).

这意味着,这里的宏DEFMACRO可以在编译文件期间改变语言及其环境.在其他语言中,编译器可能会提供特殊的编译器指令,这些指令将在编译期间被识别.有这样的定义宏的例子很多影响编译:DEFUN,DEFCLASS,DEFMETHOD,...

宏可以缩短用户代码

一个典型的例子是DEFSTRUCT用于定义类似记录的数据结构的宏.

(defstruct person name age salary)
Run Code Online (Sandbox Code Playgroud)

上面的defstruct宏创建代码

  • person具有三个槽的新结构类型
  • 用于读取和写入值的插槽访问器
  • 一个谓词来检查某个对象是否属于类 person
  • 一个make-person创建结构对象的函数
  • 印刷品

此外,它可能:

  • 记录源代码
  • 记录源代码的来源(文件,编辑器缓冲区,REPL,...)
  • 交叉引用源代码

定义结构的原始代码是一条短线.扩展代码要长得多.

DEFSTRUCT宏不需要访问语言的元级别来创建这些各种各样的事情.它只是使用典型的语言结构将一小段描述性代码转换为通常较长的定义代码.