Macros 是什么让Lisp宏如此特殊?

Macros 是什么让Lisp宏如此特殊?,macros,lisp,homoiconicity,Macros,Lisp,Homoiconicity,阅读编程语言,人们会认为这是唯一的出路。作为一名忙碌的开发人员,在其他平台上工作,我没有使用Lisp宏的特权。作为一个想了解buzz的人,请解释是什么让这个功能如此强大 请将此与我从Python、Java、C#或C开发的世界中所了解的内容联系起来。您将发现一场全面的辩论 这篇文章的一个有趣的子集: 在大多数编程语言中,语法是复杂的。宏必须分解程序语法,分析它,然后重新组装它。他们无法访问程序的解析器,因此他们必须依赖启发式和最佳猜测。有时他们的降息率分析是错误的,然后他们就崩溃了 但Lisp是不

阅读编程语言,人们会认为这是唯一的出路。作为一名忙碌的开发人员,在其他平台上工作,我没有使用Lisp宏的特权。作为一个想了解buzz的人,请解释是什么让这个功能如此强大


请将此与我从Python、Java、C#或C开发的世界中所了解的内容联系起来。

您将发现一场全面的辩论

这篇文章的一个有趣的子集:

在大多数编程语言中,语法是复杂的。宏必须分解程序语法,分析它,然后重新组装它。他们无法访问程序的解析器,因此他们必须依赖启发式和最佳猜测。有时他们的降息率分析是错误的,然后他们就崩溃了

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

下面是一个扩展示例。Lisp有一个宏,称为“setf”,用于执行赋值。setf最简单的形式是

  (setf x whatever)
它将符号“x”的值设置为表达式“whatever”的值

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

现在,如果要用新值替换列表的第一个元素,该怎么办?有一个标准函数可以实现这一点,令人难以置信的是,它的名字甚至比“汽车”还要糟糕。它是“rplaca”。但你不必记住“rplaca”,因为你可以写作

  (setf (car somelist) whatever)
(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))
把某个人的车调好

这里真正发生的是“setf”是一个宏。在编译时,它检查它的参数,并看到第一个参数的形式是(car-SOMETHING)。它对自己说:“哦,程序员正试图设置某个东西的车。用于该车的函数是‘rplaca’。”然后它悄悄地将代码重写为:

  (rplaca somelist whatever)

Lisp宏允许您决定何时(如果有)计算任何零件或表达式。举个简单的例子,想想C:

expr1 && expr2 && expr3 ...
这说明的是:评估
expr1
,如果是真的,评估
expr2
,等等

现在试着把这个
&&
变成一个函数。。。没错,你不能。比如说:

and(expr1, expr2, expr3)
(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))
(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")
无论
expr1
是否为假,都将在生成答案之前对所有三个
expr
进行评估

使用lisp宏,您可以编写如下代码:

and(expr1, expr2, expr3)
(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))
(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")
现在您有了一个
&&
,您可以像调用函数一样调用它,它不会计算您传递给它的任何表单,除非它们都是真的

要了解这是如何有用,请对比:

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

使用宏可以做的其他事情包括创建新的关键字和/或小型语言(请查看宏中的示例),将其他语言集成到lisp中,例如,您可以编写一个宏,让您可以说:

and(expr1, expr2, expr3)
(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))
(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")
而这甚至都没有进入我们的生活


希望这有帮助。

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

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

但是,由于lisp宏(本质上)是一个lisp程序,它以代码片段作为输入并返回代码来替换宏的“调用”,因此您可以根据需要扩展“原语”指令集,通常最终得到一个更可读的程序


要在C中执行同样的操作,您必须编写一个定制的预处理器,该预处理器吃掉初始(不完全是C)源代码,并吐出一些C编译器可以理解的内容。这不是一种错误的方法,但也不一定是最简单的。

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

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

这可以用来实现“sugar”语法,例如使用-子句、linq
select
-表达式等,为每个-语句
实现
,作为转换为底层代码的宏

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

下面是一段伪代码,介绍了C#中用于使用
实现
的lisp风格宏的外观:

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

请考虑用C和C++在宏和模板中做什么。它们是管理重复代码的非常有用的工具,但它们受到相当严重的限制

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

  • Lisp宏是用Lisp编写的。您可以使用Lisp编写宏
  • Lisp有一个非常规则的语法
请与精通C++的人交谈,并询问他们花了多长时间学习模板模板编程所需的模板。或是所有疯狂的把戏(
(setq2 x y (+ z 3))
(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