是否有一个简单的例子向一般的“通用”程序员解释Lisp宏?

时间:2010-12-30 00:02:52

标签: macros lisp common-lisp

我最近和一位同事谈过,并试着告诉他(Common)Lisp的美丽。我试图以某种方式解释宏,因为我认为宏是Lisp的杀手特性之一,但我失败了 - 我找不到一个很好的例子,它可以简单,简洁,可以用“凡人”来理解“程序员(十年的Java经验,一个聪明的家伙,但对”高阶“语言的经验很少)。

如果必须,您会如何通过示例解释Lisp宏?

6 个答案:

答案 0 :(得分:8)

根据我的经验,当宏看到人们如何帮助生成代码时,宏会给人们留下最好的印象,这些代码无法通过程序或其他结构来实现。很多时候,这些事情可能被描述为:

<common code>
<specific code> 
<other common code>

其中<common code>始终相同。以下是此类架构的一些示例:

<强> 1。没有宏的语言中的time宏。代码如下所示:

int startTime = getCurrentTime();
<actual code>
int endTime = getCurrentTime();
int runningTime = endTime - startTime; 

您无法将所有常用代码放入过程,因为它包含了实际代码。 (好的,您可以创建一个过程并在lambda函数中传递实际代码,如果语言支持它,但它并不总是方便)。
并且,正如您最有可能知道的那样,在Lisp中,您只需创建time宏并将实际代码传递给它:

(time 
  <actual code>) 

<强> 2。事务。要求Java程序员使用JDBC编写简单SELECT的方法 - 它将需要14-17行并包含用于打开连接和事务的代码,关闭它们,几个嵌套try-catch-finally声明和只有1或2行唯一代码 在Lisp中,您只需编写with-connection宏并将代码减少到2-3行。

第3。同步。好的,Java,C#和大多数现代语言已经有了语句,但是如果你的语言没有这样的结构怎么办?或者,如果您想引入基于STM的事务等新类型的同步?同样,您应该为此任务编写单独的类并手动使用它,即在要同步的每个语句周围放置公共代码。

这只是少数几个例子。你可以提到像with-open系列那样的“不容忘记”的宏,清理环境并保护你免受资源泄漏,新的构造宏如cond而不是多个if,当然,不要忘记像iforand这样的惰性构造,它们不会评估它们的参数(与过程应用程序相反)。

有些程序员可能会提倡,他们的语言有处理这种或那种情况的技术(ORM,AOP等),但问问他们,如果存在宏,是否需要所有这些技术?

所以,完全接受并回答关于如何解释宏的原始问题。使用Java中广泛使用的代码(C#,C ++等),将其转换为Lisp,然后将其重写为宏。

答案 1 :(得分:6)

新的WHILE声明

您的语言设计师忘记了 WHILE语句。你已经多次邮寄过他了。没有成功。你已经从语言版本2.5,2.6等待3.0。什么都没发生......

在Lisp中:

(defmacro while ...在此实施时插入你的......)

完成。

使用LOOP的简单实现需要一分钟。

从规范生成代码

然后您可能想要解析呼叫详细记录(CDR)。您有包含字段描述的记录名称。现在我可以为每个人编写类和方法。我还可以发明一些配置格式,解析配置文件并创建类。在Lisp中,我会编写一个从紧凑描述中生成代码的宏。

请参阅Domain Specific Languages in Lisp,这是一个截屏视频,展示了从工作草图到基于宏的简单泛化的典型开发周期。

代码重写

想象一下,您必须使用getter函数访问对象的插槽。现在假设您需要在某些代码区域中多次访问某些对象。由于某些原因,使用临时变量是没有办法解决的。

...
... (database-last-user database) ...
...

现在你可以写一个宏WITH-GETTER,它引入了一个getter表达式的符号。

(with-getters (database (last-user database-last-user))
   ...
   ... last-user
   ...)

宏将重写封闭块内的源,并用getter表达式替换所有指定的符号。

答案 2 :(得分:3)

由于具体的例子可能会陷入你所写的语言的细节中,所以考虑一个非具体但相关的陈述:

“您知道有时必须编写的所有样板代码吗?您永远不必在lisp中编写样板文件,因为您总是可以编写代码生成器来为您完成。”

通过'样板',我正在考虑Java中的一次性接口实现,覆盖c ++中的隐式构造函数,编写get() - set()对等等。我认为这种修辞策略可能比尝试更好直接用太多的细节解释宏,因为他可能都熟悉各种形式的样板,而他从未见过宏。

答案 3 :(得分:1)

我不太了解CL,但Scheme宏会不会?这是Scheme中的while循环:

(define-syntax while
  (syntax-rules ()
    ((while pred body ...)
     (let loop ()
       (if pred (begin body ... (loop)))))))

在这种情况下,该示例演示了您可以使用宏轻松编写自己的控件结构。 foof-loop是一个更有用的循环结构的集合(可能没有什么新的给定CL的,但仍然适合演示)。


另一个用例:从关联列表中选择值。假设用户将alist作为函数的选项传递。您可以使用此宏轻松选择值:

(define-syntax let-assq
  (syntax-rules ()
    ((let-assq alist (key) body ...)
     (let ((key (assq-ref alist 'key)))
       body ...))
    ((let-assq alist (key rest ...) body ...)
     (let ((key (assq-ref alist 'key)))
       (let-assq alist (rest ...) body ...)))))

;; Guile built-in
(define (assq-ref alist key)
  (cond ((assq key alist) => cdr)
        (else #f)))

使用示例:

(define (binary-search tree needle (lt? <))
  (let loop ((node tree))
    (and node
         (let-assq node (value left right)
           (cond ((lt? needle value) (loop left))
                 ((lt? value needle) (loop right))
                 (else value))))))

注意let-assq宏如何允许从“节点”中挑选valueleftright密钥,而不必写更长的{{1}形式。

答案 4 :(得分:0)

我将宏视为与函数类似(或双重)的抽象,除非您可以选择何时以及如何评估参数。这强调了为什么宏有用 - 就像函数一样,以防止代码重复并简化维护。

我最喜欢的例子是照应宏。像aif,awhile或aand:

  (defmacro aif (test-form then-form &optional else-form)
  `(let ((it ,test-form))
     (if it ,then-form ,else-form)))

  (defmacro awhile (expr &body body)
      `(do ((it ,expr ,expr)) ((not it))
         ,@body))

    (defmacro aand (&rest args)
      (cond 
        ((null args) t)
        ((null (cdr args)) (car args))
        (t `(aif ,(car args) (aand ,@(cdr args))))))

这些非常简单,可以省去很多打字。

答案 5 :(得分:0)

这不是你可以在短时间内解释的东西,好吧,宏的概念可以用一个句子来解释,而像一段时间的例子相当容易理解,问题是那个人会我真的不明白为什么只用这些微不足道的例子来表示宏是一件好事。