了解如何实现一次性Lisp宏 [英] Understanding how to implement once-only lisp macro

查看:67
本文介绍了了解如何实现一次性Lisp宏的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在Peter Seibel的书"Practical Common Lisp"中,我们可以一次找到非常复杂的宏的定义(请参见解决方案

您在看这个吗?

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

并不是那么复杂,但是它确实有一个嵌套的反引号,并且多个级别彼此相似,即使对于有经验的Lisp编码人员,也容易造成混淆.

这是一个宏,宏用于编写其扩展:一个宏,用于写入宏主体的一部分.

宏本身的主体中有一个普通的let,然后生成了一个用引号引起来的let,它将驻留在使用once-only的宏主体中.最后,在用户使用该宏的代码站点的那个宏的宏扩展中将出现一个双反引号let.

两次生成gensyms都是必要的,因为once-only本身是一个宏,因此必须出于自身的原因而卫生.因此它会在最外面的let中为其自身生成一堆gensyms.而且,once-only的目的是简化另一个卫生宏的编写.因此它也为该宏生成gensyms.

简而言之,once-only需要创建一个宏扩展,该宏扩展需要一些值为gensyms的局部变量.这些局部变量将用于将gensyms插入另一个宏扩展中,以使其卫生.这些局部变量本身是卫生的,因为它们是宏扩展,因此它们也是基因符号.

如果您正在编写一个普通宏,则您的本地变量包含gensyms,例如:

;; silly example
(defmacro repeat-times (count-form &body forms)
  (let ((counter-sym (gensym)))
    `(loop for ,counter-sym below ,count-form do ,@forms)))

在编写宏的过程中,您已经发明了符号counter-sym.该变量在普通视图中定义.您(人类)选择它的方式不会与词汇范围内的任何内容发生冲突.有问题的词汇范围是您的宏的范围.我们不必担心counter-sym会意外地捕获count-formforms内的引用,因为forms只是要放入一段代码中的数据,这些数据最终将插入到某些远程词法范围内(该站点使用宏的位置).我们确实要担心不要将counter-sym与宏中的另一个变量混淆.例如,我们不能给我们的局部变量起名字count-form.为什么?因为该名称是我们的函数参数之一;我们将其隐藏起来,从而产生编程错误.

现在,如果您想使用宏来帮助您编写该宏,则计算机必须完成与您相同的工作.在编写代码时,它必须发明一个变量名,并且必须小心使用它的名字.

但是,与您不同,代码编写器看不到周围的作用域.它不能简单地查看存在哪些变量并选择不会发生冲突的变量.机器只是一个函数,它接受一些参数(未评估的代码段)并生成一段代码,然后在机器完成工作后将其盲目地替换为作用域.

因此,机器必须更明智地选择名称.实际上,要完全防弹,必须具有偏执狂,并使用完全唯一的符号:gensyms.

因此,继续进行示例,假设我们有一个机器人将为我们编写此宏主体.该机器人可以是一个宏,repeat-times-writing-robot:

(defmacro repeat-times (count-form &body forms)
  (repeat-times-writing-robot count-form forms))  ;; macro call

机器人宏会是什么样?

(defmacro repeat-times-writing-robot (count-form forms)
  (let ((counter-sym-sym (gensym)))     ;; robot's gensym
    `(let ((,counter-sym-sym (gensym))) ;; the ultimate gensym for the loop
      `(loop for ,,counter-sym-sym below ,,count-form do ,@,forms))))

您可以看到它如何具有once-only的某些功能:双重嵌套和(gensym)的两个层次.如果您能理解这一点,那么跳转到once-only的机会就很小.

当然,如果我们只想让机器人编写重复次数,就可以将其设为一个函数,然后该函数就不必担心发明变量:它不是宏,因此不需要需要卫生:

 ;; i.e. regular code refactoring: a piece of code is moved into a helper function
 (defun repeat-times-writing-robot (count-form forms)
   (let ((counter-sym (gensym)))
     `(loop for ,counter-sym below ,count-form do ,@forms)))

 ;; ... and then called:
(defmacro repeat-times (count-form &body forms)
  (repeat-times-writing-robot count-form forms))  ;; just a function now

但是once-only 不能是一个函数,因为它的 job 是代表其老板(使用它的宏)发明变量,而函数不能引入变量进入呼叫者.

In Peter Seibel's book "Practical Common Lisp", we can find the definition of the very complicated macro once-only (see the bottom of page http://www.gigamonkeys.com/book/macros-defining-your-own.html).

I'm reading this macro definition for the 10th times in last 3 weeks and cannot understand how it works. :( Worse, I cannot develop this macro on my own, even though I understand its purpose and how to use it.

I'm especially interested in systematic "derivation" of this notoriously hard macro, step by step! Any help?

解决方案

Are you looking at this:

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

It's not that complicated, but it does have a nested backquote, and multiple levels which are similar to each other, leading to easy confusion, even for experienced Lisp coders.

This is a macro which is used by macros for writing their expansions: a macro which writes parts of the bodies of macros.

There is a plain let in the body of the macro itself, then a once-backquoted generated let which will live inside the body of the macro which usess once-only. Finally, there is a doubly backquoted let which will appear in the macro expansion of that macro, in the code site where the macro is used by the user.

The two rounds of generating gensyms are necessary because once-only is a macro itself, and so it has to be hygienic for its own sake; so it generates a bunch of gensyms for itself in the outermost let. But also, the purpose of once-only is to simplify the writing of another hygienic macro. So it generates gensyms for that macro also.

In a nutshell, once-only needs to create a macro-expansion which requires some local variables whose values are gensyms. Those local variables will be used to insert the gensyms into another macro expansion to make it hygienic. And those local variables have to themselves be hygienic since they are a macro expansion, so they are also gensyms.

If you're writing a plain macro, you have local variables which hold gensyms, e.g.:

;; silly example
(defmacro repeat-times (count-form &body forms)
  (let ((counter-sym (gensym)))
    `(loop for ,counter-sym below ,count-form do ,@forms)))

In the process of writing the macro, you have invented a symbol, counter-sym. This variable is defined in plain view. You, the human, have chosen it in such a way that it does not clash with anything in the lexical scope. The lexical scope in question is that of your macro. We don't have to worry about counter-sym accidentally capturing references inside count-form or forms because forms are just data which is going into a piece of code which will end up inserted in some remote lexical scope (the site where the macro is used). We do have to worry about not confusing counter-sym with another variable inside our macro. For instance, we cannot give our local variable the name count-form. Why? Because that name is one of our function arguments; we would shadow it, creating a programming error.

Now if you want a macro to help you write that macro, then the machine has to do the same job as you. When it is writing code, it has to invent a variable name, and it has to be careful about what name it invents.

However, the code-writing machine, unlike you, does not see the surrounding scope. It cannot simply look at what variables are there and choose ones which do not clash. The machine is just a function which takes some arguments (pieces of unevaluated code) and produces a piece of code that is then blindly substituted into a scope after that machine has done its job.

Therefore, the machine has to choose the names extra wisely. In fact, to be completely bullet proof, it has to be paranoid and use symbols which are completely unique: gensyms.

So continuing with the example, suppose we have a robot which will write this macro body for us. That robot can be a macro, repeat-times-writing-robot:

(defmacro repeat-times (count-form &body forms)
  (repeat-times-writing-robot count-form forms))  ;; macro call

What might the robot macro look like?

(defmacro repeat-times-writing-robot (count-form forms)
  (let ((counter-sym-sym (gensym)))     ;; robot's gensym
    `(let ((,counter-sym-sym (gensym))) ;; the ultimate gensym for the loop
      `(loop for ,,counter-sym-sym below ,,count-form do ,@,forms))))

You can see how this has some of the features of once-only: the double nesting and the two levels of (gensym). If you can understand this, then the leap to once-only is small.

Of course, if we just wanted a robot to write repeat-times, we would make it a function, and then that function wouldn't have to worry about inventing variables: it is not a macro and so it doesn't need hygiene:

 ;; i.e. regular code refactoring: a piece of code is moved into a helper function
 (defun repeat-times-writing-robot (count-form forms)
   (let ((counter-sym (gensym)))
     `(loop for ,counter-sym below ,count-form do ,@forms)))

 ;; ... and then called:
(defmacro repeat-times (count-form &body forms)
  (repeat-times-writing-robot count-form forms))  ;; just a function now

But once-only cannot be a function because its job is to invent variables on behalf of its boss, the macro which uses it, and a function cannot introduce variables into its caller.

这篇关于了解如何实现一次性Lisp宏的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆