首页 文章

是什么让Lisp宏如此特别?

提问于
浏览
266

阅读编程语言的Paul Graham's essays,人们会认为Lisp macros是唯一的出路 . 作为一个忙于开发人员,在其他平台上工作,我没有使用Lisp宏的特权 . 作为想要了解嗡嗡声的人,请解释是什么让这个功能如此强大 .

还请将此与我从Python,Java,C#或C开发世界中理解的内容联系起来 .

14 回答

  • 8

    简而言之,宏用于定义Common Lisp或Domain Specific Languages(DSL)的语言语法扩展 . 这些语言嵌入到现有的Lisp代码中 . 现在,DSL可以具有类似于Lisp的语法(如Peter Norvig的Prolog Interpreter用于Common Lisp)或完全不同(例如,用于Clojure的Infix Notation Math) .

    这是一个更具体的例子:
    Python具有内置于该语言中的列表推导 . 这为常见案例提供了简单的语法 . 这条线

    divisibleByTwo = [x for x in range(10) if x % 2 == 0]
    

    产生一个包含0到9之间所有偶数的列表 . 回到Python 1.5天没有这样的语法;你会使用更像这样的东西:

    divisibleByTwo = []
    for x in range( 10 ):
       if x % 2 == 0:
          divisibleByTwo.append( x )
    

    这些都是功能相同的 . 让我们暂停怀疑并假装Lisp有一个非常有限的循环宏,它只是迭代而没有简单的方法来完成相当于列表推导 .

    在Lisp中,您可以编写以下内容 . 我应该注意这个人为的例子与Python代码相同,而不是Lisp代码的一个很好的例子 .

    ;; the following two functions just make equivalent of Python's range function
    ;; you can safely ignore them unless you are running this code
    (defun range-helper (x)
      (if (= x 0)
          (list x)
          (cons x (range-helper (- x 1)))))
    
    (defun range (x)
      (reverse (range-helper (- x 1))))
    
    ;; equivalent to the python example:
    ;; define a variable
    (defvar divisibleByTwo nil)
    
    ;; loop from 0 upto and including 9
    (loop for x in (range 10)
       ;; test for divisibility by two
       if (= (mod x 2) 0) 
       ;; append to the list
       do (setq divisibleByTwo (append divisibleByTwo (list x))))
    

    在我走得更远之前,我应该更好地解释宏是什么 . 它是按代码对代码执行的转换 . 也就是说,由解释器(或编译器)读取的一段代码,它接受代码作为参数,操作并返回结果,然后就地运行 .

    当然,很多打字和程序员都很懒惰 . 所以我们可以定义DSL来做列表推导 . 实际上,我们已经使用了一个宏(循环宏) .

    Lisp定义了几种特殊的语法形式 . 引号( ' )表示下一个标记是文字 . quasiquote或反引号( ``` )表示下一个标记是带有转义的文字 . 转义由逗号运算符指示 . 文字 '(1 2 3) 相当于Python的 [1, 2, 3] . 您可以将其分配给另一个变量或在适当的位置使用它 . 你可以认为 ``(1 2 ,x)等同于Python的[1, 2, x],其中x` 是先前定义的变量 . 这个列表符号是进入宏的魔力的一部分 . 第二部分是Lisp阅读器,它智能地将宏替换为代码,但最好说明如下:

    所以我们可以定义一个名为 lcomp 的宏(列表推导的简称) . 它的语法与我们在示例中使用的python完全相同 [x for x in range(10) if x % 2 == 0] - (lcomp x for x in (range 10) if (= (% x 2) 0))

    (defmacro lcomp (expression for var in list conditional conditional-test)
      ;; create a unique variable name for the result
      (let ((result (gensym)))
        ;; the arguments are really code so we can substitute them 
        ;; store nil in the unique variable name generated above
        `(let ((,result nil))
           ;; var is a variable name
           ;; list is the list literal we are suppose to iterate over
           (loop for ,var in ,list
                ;; conditional is if or unless
                ;; conditional-test is (= (mod x 2) 0) in our examples
                ,conditional ,conditional-test
                ;; and this is the action from the earlier lisp example
                ;; result = result + [x] in python
                do (setq ,result (append ,result (list ,expression))))
               ;; return the result 
           ,result)))
    

    现在我们可以在命令行执行:

    CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
    (0 2 4 6 8)
    

    挺整洁的,对吧?现在它没有't stop there. You have a mechanism, or a paintbrush, if you like. You can have any syntax you could possibly want. Like Python or C#' s with 语法 . 或.NET的LINQ语法 . 最后,这是吸引人们使用Lisp的最佳灵活性 .

  • 10

    你会发现围绕lisp macro here的全面辩论 .

    该文章的一个有趣的子集:

    在大多数编程语言中,语法很复杂 . 宏必须拆分程序语法,分析它并重新组装它 . 他们无法访问程序的解析器,因此他们必须依赖于启发式和最佳猜测 . 有时他们的降价分析是错误的,然后他们就会破裂 . 但是Lisp是不同的 . Lisp宏可以访问解析器,它是一个非常简单的解析器 . Lisp宏不是一个字符串,而是一个列表形式的预处理源代码,因为Lisp程序的源不是字符串;这是一个清单 . Lisp程序非常擅长拆分列表并将它们重新组合在一起 . 他们每天都可靠地做到这一点 . 这是一个扩展的例子 . Lisp有一个名为“setf”的宏,它执行赋值 . 最简单的setf形式是(setf x what)
    它将符号“x”的值设置为表达式“whatever”的值 . Lisp也有名单;您可以使用“car”和“cdr”函数分别获取列表的第一个元素或列表的其余部分 . 现在,如果您想用新值替换列表的第一个元素,该怎么办?这样做有一个标准功能,令人难以置信的是,它的名字甚至比“汽车”更糟糕 . 这是“rplaca” . 但你不必记住“rplaca”,因为你可以写(setf(car somelist)无论如何)
    设置汽车somelist . 这里真正发生的是“setf”是一个宏 . 在编译时,它检查它的参数,并且它看到第一个具有形式(汽车SOMETHING) . 它对自己说“哦,程序员正试图设置汽车的东西 . 用于此的功能是'rplaca' . ”它悄悄地将代码改写为:( rplaca somelist whatever)

  • 9

    Common Lisp宏本质上扩展了代码的“语法原语” .

    例如,在C中,switch / case结构只适用于整数类型,如果你想将它用于浮点数或字符串,你可以使用嵌套的if语句和显式比较 . 你也无法编写C宏来为你完成这项工作 .

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

    要在C中执行相同的操作,您必须编写一个自定义预处理器,它会占用您的初始(非C)源并吐出C编译器可以理解的内容 . 这不是一个错误的方法,但它不一定是最容易的 .

  • 28

    Lisp宏允许您决定何时(如果有的话)将评估任何部分或表达式 . 举一个简单的例子,想想C:

    expr1 && expr2 && expr3 ...
    

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

    现在尝试将这个 && 变成一个函数......这是对的,你做不到 . 打电话给:

    and(expr1, expr2, expr3)
    

    无论 expr1 是否为假,都会在得到答案之前评估所有三个 exprs

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

    (defmacro && (expr1 &rest exprs)
        `(if ,expr1                     ;` Warning: I have not tested
             (&& ,@exprs)               ;   this and might be wrong!
             nil))
    

    现在你有一个 && ,你可以像一个函数一样调用它,它不会评估你传递给它的任何形式,除非它们都是真的 .

    对比,看看这是如何有用的:

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

    和:

    and(very_cheap_operation(),
        very_expensive_operation(),
        operation_with_serious_side_effects());
    

    您可以使用宏执行的其他操作是创建新关键字和/或迷你语言(例如,查看(loop ...)宏),将其他语言集成到lisp中,例如,您可以编写一个宏,让您说出如下内容:

    (setvar *rows* (sql select count(*)
                          from some-table
                         where column1 = "Yes"
                           and column2 like "some%string%")
    

    而那甚至没有进入Reader macros .

    希望这可以帮助 .

  • 8

    我没有看到Lisp宏解释得比这个家伙更好:http://www.defmacro.org/ramblings/lisp.html

  • 2

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

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

    这可用于实现"sugar"语法,如 for each -statement using -clause,linq select -expressions等,作为转换为底层代码的宏 .

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

    下面是C#中用于实现 using 的lisp样式宏如何看起来的伪代码:

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

    想想您可以使用宏和模板在C或C中执行的操作 . 它们是管理重复代码的非常有用的工具,但它们受到严格限制 .

    • 有限的宏/模板语法限制了它们的使用 . 例如,您可以轻松维护内部数据 .

    • C和C的复杂,非常不规则的语法使得编写非常通用的宏变得困难 .

    Lisp和Lisp宏解决了这些问题 .

    • Lisp宏是用Lisp编写的 . 你有Lisp的全部功能来编写宏 .

    • Lisp有一个非常规则的语法 .

    与任何掌握了C语言的人交谈并询问他们花了多长时间学习模板元编程所需的模板技巧 . 或者像Modern C Design这样的(优秀)书籍中的所有疯狂技巧,即使语言已经标准化了十年,仍然很难调试和(在实践中)在现实编译器之间不可移植 . 如果你用于元编程的语言与你用于编程的语言相同,那么所有这些都会消失!

  • 51

    我不确定我是否可以为每个人的(优秀)帖子添加一些见解,但......

    由于Lisp语法,Lisp宏工作得很好性质 .

    Lisp是一种 extremely regular 语言(想想一切都是 list );宏使您可以将数据和代码视为相同(不需要字符串解析或其他黑客来修改lisp表达式) . 您将这两个功能结合起来,并且有一种非常方便的方法来修改代码 .

    Edit: 我想说的是Lisp是homoiconic,这意味着lisp程序的数据结构是用lisp本身编写的 .

    因此,您最终会使用语言本身及其所有功能在语言之上创建自己的代码生成器(例如,在Java中,您必须通过字节码编织来破解您的方式,尽管像AspectJ这样的某些框架允许您使用不同的方法做到这一点,它从根本上说是一个黑客攻击) .

    在实践中,使用宏,您最终可以在lisp之上构建自己的迷你语言,而无需学习其他语言或工具,并且可以充分利用语言本身的强大功能 .

  • 5

    既然现有的答案提供了很好的具体例子来解释宏实现了什么以及如何实现,也许它有助于汇集一些关于为什么宏观设施相对于其他语言获得显着收益的想法;首先来自这些答案,然后是来自其他地方的伟大答案:

    ...在C中,你必须编写一个自定义的预处理器[这可能有资格作为一个足够复杂的C程序] ......

    与任何掌握了C语言的人交谈,并询问他们花了多长时间学习模板元编程所需的所有模板技巧[仍然没有那么强大] .

    ...在Java中你必须使用字节码编织来破解你的方式,尽管像AspectJ这样的一些框架允许你使用不同的方法来实现这一点,但它从根本上说是一个黑客攻击 .

    DOLIST类似于Perl的foreach或Python的 . 作为JSR-201的一部分,Java在Java 1.5中添加了类似的循环结构和“增强”for循环 . 注意宏有什么区别 . 一个Lisp程序员在他们的代码中注意到一个共同的模式,可以编写一个宏来给自己提供该模式的源级抽象 . 注意到相同模式的Java程序员必须说服Sun,这种特殊的抽象值得添加到该语言中 . 然后Sun必须发布一个JSR并召集一个行业范围的“专家组”来解决所有问题 . 根据Sun的说法,这个过程平均需要18个月 . 之后,编译器编写者都必须升级他们的编译器以支持新功能 . 甚至一旦Java程序员最喜欢的编译器支持新版本的Java,他们可能“仍然”不能使用新功能,直到它们被允许破坏与旧版Java的源兼容性 . 因此,Common Lisp程序员可以在五分钟内自行解决的烦恼困扰着Java程序员多年 .

  • 38

    Lisp宏代表几乎任何大型编程项目中出现的模式 . 最终在一个大型程序中你有一段代码,你会发现它会更简单,更容易出错,你可以编写一个程序,输出源代码作为文本然后你可以粘贴 .

    在Python中,对象有两个方法 __repr____str__ . __str__ 只是人类可读的表示 . __repr__ 返回一个有效Python代码的表示,也就是说,可以作为有效的Python输入到解释器中的东西 . 通过这种方式,您可以创建一小段Python,生成可以粘贴到实际源代码中的有效代码 .

    在Lisp中,整个过程已由宏系统正式化 . 当然它可以让你创建语法的扩展并做各种奇特的事情,但它的实际用处总结如上 . 当然,Lisp宏系统允许您使用整个语言的全部功能来操纵这些“片段” .

  • 0

    简而言之,宏是代码的转换 . 它们允许引入许多新的语法结构 . 例如,考虑C#中的LINQ . 在lisp中,存在由宏实现的类似语言扩展(例如,内置循环构造,迭代) . 宏显着减少了代码重复 . 宏允许嵌入«小语言»(例如,在c#/ java中使用xml进行配置,在lisp中,使用宏可以实现相同的功能) . 宏可能会隐藏使用库的困难 .

    例如,在lisp中你可以写

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

    和这隐藏了所有数据库内容(事务,正确连接关闭,获取数据等),而在C#中,这需要创建SqlConnections,SqlCommands,将SqlParameters添加到SqlCommands,在SqlDataReaders上循环,正确关闭它们 .

  • 269

    虽然上面都解释了什么是宏,甚至有很酷的例子,但我认为宏和普通函数之间的关键区别在于LISP在调用函数之前首先计算所有参数 . 对于宏,它是相反的,LISP将未评估的参数传递给宏 . 例如,如果将(1 2)传递给函数,函数将接收值3.如果将其传递给宏,它将接收一个List(1 2) . 这可以用来做各种非常有用的东西 .

    • 添加新的控制结构,例如循环或解构列表

    • 测量执行传入函数所需的时间 . 使用函数,在控制传递给函数之前,将评估参数 . 使用宏,您可以在秒表的开始和结束之间拼接代码 . 下面在宏和函数中具有完全相同的代码,输出非常不同 . 注意:这是一个人为的例子,选择实现是为了更好地突出差异 .

    (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
    
  • -2

    我从普通的lisp食谱中得到了这个,但我认为它解释了为什么lisp宏好的方式 .

    “宏是一段普通的Lisp代码,它运行在另一个推定的Lisp代码上,将其转换为(更接近于可执行Lisp的版本) . 这可能听起来有点复杂,所以让我们举一个简单的例子 . 假设你想要一个setq的两个版本将两个变量设置为相同的值 . 所以如果你写的话

    (setq2 x y (+ z 3))
    

    z=8 时,x和y都设置为11.(我可以't think of any use for this, but it'只是一个例子 . )

    很明显,我们不能将setq2定义为函数 . 如果 x=50y=-5 ,此函数将接收值50,-5和11;它不知道应该设置哪些变量 . 我们真正想说的是,当你(Lisp系统)看到 (setq2 v1 v2 e) 时,将它视为等同于 (progn (setq v1 e) (setq v2 e)) . 实际上,这不太对,但它现在会做 . 宏允许我们通过指定用于将输入模式 (setq2 v1 v2 e) “转换为输出模式 (progn ...) ”的程序来精确地执行此操作 .

    如果你认为这很好,你可以继续在这里阅读:http://cl-cookbook.sourceforge.net/macros.html

  • 97

    在python中你有装饰器,你基本上有一个函数,它将另一个函数作为输入 . 你可以做任何你想做的事情:调用函数,做其他事情,在资源获取版本中包装函数调用等等,但你不能窥探那个函数 . 假设我们想让它更强大,说你的装饰器收到函数的代码作为列表然后你不仅可以执行函数,但你现在可以执行它的一部分,重新排序函数的行等 .

相关问题