有没有一个简单的例子来解释 Lisp 宏到“通用”?程序员?

发布于 2024-10-09 12:33:30 字数 185 浏览 0 评论 0原文

我最近与一位同事交谈,并尝试告诉他(Common)Lisp 的美妙之处。我试图以某种方式解释宏,因为我认为宏是 Lisp 的杀手级功能之一,但我失败得很惨——我找不到一个简短、简洁且可以被“凡人”理解的好例子。 “程序员(十年的 Java 经验,一个聪明人,但对“高阶”语言的经验很少)。

如果必须的话,你会如何通过例子解释 Lisp 宏?

I was having a conversation with a colleague recently and tried telling him about the beauty of (Common) Lisp. I tried to explain macros somehow, since I consider macros one of the killer features of Lisp, but I failed rather miserably -- I couldn't find a good example which would be short, concise and understandable by a "mere mortal" programmer (decade of Java experience, a bright guy altogether, but very little experience with "higher-order" languages).

How would you explain Lisp macros by example if you had to?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

昵称有卵用 2024-10-16 12:33:31

根据我的经验,当人们看到宏如何帮助生成过程或其他构造无法生成的代码时,宏会给他们留下最好的印象。通常这样的事情可以被描述为:

<common code>
<specific code> 
<other 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,然后将其重写为宏。

From my experience, macros make the best impression on people when they see how it helps to produce code, that cannot be made by the procedures or other constructs. Very often such things may be described as:

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

where <common code> is always the same. Here are some examples of such schema:

1. The time macro. Code in a language without macros will look something like this:

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

You cannot put all common code to procedure, since it wraps actual code around. (OK, you can make a procedure and pass actual code in lambda function, if the language supports it, but it is not always convenient).
And, as you most probably know, in Lisp you just create time macro and pass actual code to it:

(time 
  <actual code>) 

2. Transactions. Ask Java-programmer to write method for simple SELECT with JDBC - it will take 14-17 lines and include code to open connection and transaction, to close them, several nested try-catch-finally statements and only 1 or 2 lines of unique code.
In Lisp you just write with-connection macro and reduce code to 2-3 lines.

3. Synchronization. OK, Java, C# and most of the modern languages already have statements for it, but what to do if your language doesn't have such a construct? Or if you want to introduce new kind of synchronization like STM-based transactions? Again, you should write separate class for this task and work with it manually, i.e. put common code around each statement you want to synchronize.

That was only few examples. You can mention "not-to-forget" macros like with-open series, that clean-up environment and protect you from recourses leaks, new constructs macros like cond instead of multiple ifs, and, of course, don't forget about lazy constructs like if, or and and, that do not evaluate their arguments (in opposite to procedure application).

Some programmers may advocate, that their language has a technology to treat this or that case (ORM, AOP, etc), but ask them, would all these technologies be needed, if macros existed?

So, taking it altogether and answering original question about how to explain macros. Take any widely-used code in Java (C#, C++, etc), transform it to Lisp and then rewrite it as a macro.

娇女薄笑 2024-10-16 12:33:31

新的 WHILE 语句

您的语言设计者忘记了 WHILE 语句。你已经给他发过好几次邮件了。没有成功。从语言版本 2.5、2.6 到 3.0,您一直在等待。什么也没发生...

在 Lisp 中:

(defmacro while ...在此处插入 while 实现...)

完成。

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

根据规范生成代码

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

请参阅 Lisp 中的领域特定语言,这是一个截屏视频,展示了从工作草图到简单的基于宏的概括的典型开发周期。

代码重写

想象一下,您必须使用 getter 函数访问对象的槽。现在假设您需要在某个代码区域多次访问某些对象。由于某种原因,使用临时变量并不是解决方案。

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

现在您可以编写一个WITH-GETTER 宏,它为getter 表达式引入一个符号。

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

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

New WHILE statement

Your language designer forgot a WHILE statement. You've mailed him several times. No success. You've waited from language version 2.5, 2.6 to 3.0. Nothing happened...

In Lisp:

(defmacro while ... insert your while implementation here ...)

Done.

The trivial implementation using LOOP takes a minute.

Code generation from specifications

Then you may want to parse call detail records (CDRs). You have record names with field descriptions. Now I can write classes and methods for each of those. I could also invent some configuration format, parse a configuration file and create the classes. In Lisp I would write a macro that generates the code from a compact description.

See Domain Specific Languages in Lisp, a screencast showing a typical development cycle from a working sketch to a simple macro based generalization.

Code rewriting

Imagine that you have to access slots of objects using getter functions. Now imagine that you need to access some objects multiple times in some code region. For some reason using temporary variables is no solution.

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

Now you could write a macro WITH-GETTER which introduces a symbol for the getter expression.

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

The macro would rewrite the source inside the enclosed block and replace all specified symbols with the getter expression.

巴黎盛开的樱花 2024-10-16 12:33:31

由于具体的示例可能会陷入您编写它们所用语言的细节中,请考虑一个不具体但相关的陈述:

“您知道有时必须编写的所有样板代码吗?您永远不必用以下语言编写样板代码 : lisp,因为你总是可以编写一个代码生成器来为你做这件事。”

通过“样板”,我正在考虑 Java 中的一次性接口实现、重写 C++ 中的隐式构造函数、编写 get()-set() 对等。我认为这种修辞策略可能比尝试直接解释宏更有效过于详细,因为他可能非常熟悉各种形式的样板文件,而他从未见过宏。

Since concrete examples can get bogged down in the details of the language you're writing them in, consider a non-concrete but relatable statement:

"You know all that boilerplate code that you sometimes have to write? You never have to write boilerplate in lisp, since you can always write a code-generator to do it for you."

By 'boilerplate', I'm thinking of one-off interface implementations in Java, overriding implicit constructors in c++, writing get()-set() pairs, etc. I think this rhetorical strategy might work better than trying to explain macros directly in too much detail, since he's probably all too familiar with various forms of boilerplate, whereas he's never seen a macro.

好听的两个字的网名 2024-10-16 12:33:31

我不太了解 CL,但是Scheme 宏可以吗?这是Scheme 中的一个while 循环:

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

在本例中,该示例演示了您可以轻松地使用宏编写自己的控制结构。 foof-loop 是更多内容的集合有用的循环结构(对于 CL 来说可能没什么新意,但仍然适合演示)。


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

(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 宏如何允许选取 valueleft、和“节点”中的右键键,而无需编写更长的let形式。

I don't know CL well enough, but will Scheme macros do? Here's a while loop in Scheme:

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

In this case, the example demonstrates that you can easily write your own control structures using macros. foof-loop is a collection of even more useful looping constructs (probably nothing new given CL's ones, but still good for a demonstration).


Another use case: picking values out of associative lists. Say users pass in an alist as options to your function. You can easily pick values out by using this macro:

(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)))

Example usage:

(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))))))

Notice how the let-assq macro allows picking out the value, left, and right keys from the "node" without having to write a much longer let form.

江挽川 2024-10-16 12:33:31

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

我最喜欢的例子是照应宏。像 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))))))

这些非常简单,可以节省大量打字时间。

I view macros as an abstraction similar (or dual) to functions, except you may choose when and how to evaluate the arguments. This underlines why macros are useful - just like functions are, to prevent code duplication and to ease maintenance.

My favorite example are anaphoric macros. Like aif, awhile or 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))))))

These are very simple and can save a lot of typing.

等往事风中吹 2024-10-16 12:33:31

这不是你能在短时间内解释清楚的事情,是的,宏的概念可以用一句话来解释,一个例子,比如一段时间,还是很容易理解的,问题是那个人不会真正理解为什么宏是一个很好的东西,只有这样简单的例子。

It's not something you can explain in a short amount of time, well it is, the concept of macro can be explained in one sentence and an example such as a while is fairly easy to understand, the problem is that that person will not really understand why macros are a nice thing to have with only such trivial examples.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文