Lisp宏有多有用?


22

Common Lisp允许您编写执行所需源转换的宏。

Scheme为您提供了一个卫生的模式匹配系统,您还可以执行转换。宏在实践中有多有用?保罗·格雷厄姆(Paul Graham)在击败平均水平中说:

Viaweb编辑器的源代码大概是20-25%的宏。

人们实际上最终会用宏做什么事情?


我认为这绝对是主观的,我已经编辑了您的格式问题。这可能 是重复的,但我找不到。
Tim Post

1
我想,似乎所有重复的功能似乎都不适合某个功能。

2
您可以使用宏将Lisp转换为 具有任何语法和语义的任何其他语言:bit.ly/vqqvHU
SK-logic

programmers.stackexchange.com/questions/81202/…在这里值得一看,但不是重复的。
David Thornley

Answers:


15

看看Matthias Felleisen在2002年的LL1讨论列表上的帖子。他建议了宏的三种主要用法:

  1. 数据子语言:我可以编写简单的表达式并创建带有引号,取消引号等的复杂嵌套列表/数组/表。
  2. 绑定构造:我可以使用宏介绍新的绑定构造。这帮助我摆脱了lambda,并将彼此在一起的东西放得更近。
  3. 评估重新排序:我可以根据需要引入延迟/推迟对表达式进行评估的构造。考虑循环,新条件,延迟/强制等。[注意:在Haskell或任何惰性语言中,这是不必要的。

18

我主要使用宏来添加节省时间的新语言结构,否则将需要大量样板代码。

例如,我最近发现自己想要for-loop与C ++ / Java类似的命令。但是,作为一种功能性语言,Clojure并没有开箱即用。所以我只是将其实现为一个宏:

(defmacro for-loop [[sym init check change :as params] & steps]
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#)))

现在我可以做:

 (for-loop [i 0 , (< i 10) , (inc i)] 
   (println i))

在那里,您可以用六行代码构建一个新的通用编译时语言。


13

人们实际上最终会用宏做什么事情?

编写语言扩展或DSL。

要使用类似Lisp的语言来感受一下,请学习Racket,它具有多种语言变体:Typed Racket,R6RS和Datalog。

另请参见Boo语言,语言可让您访问编译器管道,以达到通过宏创建特定于域的语言的特定目的。


4

这里有些例子:

方案:

  • define用于功能定义。基本上,它是定义函数的较短方法。
  • let 用于创建词法范围变量。

Clojure:

  • defn,根据其文档:

    与(def name(fn [params *] exprs *))或(def name(fn([params *] exprs *)+))相同,其中任何文档字符串或attrs添加到var元数据

  • for:列表理解
  • defmacro:具有讽刺意味?
  • defmethoddefmulti:使用多种方法
  • ns

这些宏很多使在抽象级别上编写代码变得容易得多。我认为宏在很多方面都类似于非Lisps中的语法。

绘图库Incanter提供了一些复杂数据操作的宏。


4

宏对于嵌入某些模式很有用。

例如,Common Lisp没有定义while循环,但是有do 一个可用于定义循环。

这是On Lisp的示例 。

(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

(let ((a 0))
  (while (< a 10)
    (princ (incf a))))

这将打印“ 12345678910”,并且如果您尝试查看会发生什么 macroexpand-1

(macroexpand-1 '(while (< a 10) (princ (incf a))))

这将返回:

(DO () ((NOT (< A 10))) (PRINC (INCF A)))

这是一个简单的宏,但是如前所述,它们通常用于定义新的语言或DSL,但是从这个简单的示例中,您已经可以尝试想象如何使用它们。

loop宏是一个什么样的宏可以做一个很好的例子。

(loop for i from 0 to 10
      if (and (= (mod i 2) 0) i)
        collect it)
=> (0 2 4 6 8 10)
(loop for i downfrom 10 to 0
      with a = 2
      collect (* a i))
=> (20 18 16 14 12 10 8 6 4 2 0)               

Common Lisp还有另一种称为读取器宏的宏,可用于修改读取器解释代码的方式,即,您可以使用它们来使用#{和#}并使用诸如#(和#)之类的定界符。


3

这是我用于调试(在Clojure中)的一个:

user=> (defmacro print-var [varname] `(println ~(name varname) "=" ~varname))
#'user/print-var
=> (def x (reduce * [1 2 3 4 5]))
#'user/x
=> (print-var x)
x = 120
nil

我不得不处理C ++中的手动哈希表,其中该get方法将非常量字符串引用作为参数,这意味着我不能使用文字来调用它。为了使处理起来更容易,我写了如下内容:

#define LET(name, value, body)  \
    do {                        \
        string name(value);     \
        body;                   \
        assert(name == value);  \
    } while (false)

尽管不太可能出现类似此问题的情况,但我发现您可以使用不对两次自变量进行两次求值的宏(例如通过引入真正的 let绑定)特别好。(承认,在这里我可以解决)。

我还求助于将东西包裹起来的丑陋骇客方式,do ... while (false)这样您就可以在if的then部分中使用它,并且仍然可以按预期进行else部分的工作。您不需要Lisp,这是在语法树上操作的宏的功能,而不是字符串(或字符串,在C和C ++的情况下,我认为是令牌序列),然后进行解析。

有一些内置的线程宏可用于重组代码,使代码读起来更清晰(“线程”就像“将代码放在一起”,而不是并行性)。例如:

(->> (range 6) (filter even?) (map inc) (reduce *))

它采用第一种形式,(range 6)并使其成为下一种形式的最后一个参数,进而使之成为下一种形式(filter even?)的最后一个参数,依此类推,以便将上面的内容重写为

(reduce * (map inc (filter even? (range 6))))

我认为第一个读起来更清楚:“获取这些数据,对其进行处理,然后执行该操作,然后再进行其他操作,我们就完成了”,但这是主观的;客观上是正确的是,您按执行的顺序阅读操作(忽略懒惰)。

还有一个变体,它将前一个形式作为第一个(而不是最后一个)参数插入。一种用例是算术:

(-> 17 (- 2) (/ 3))

读为“取17,减去2并除以3”。

说到算术,您可以编写一个进行后缀符号解析的宏,这样您就可以说出例如,(infix (17 - 2) / 3)并且它会吐出来(/ (- 17 2) 3),它的缺点是可读性差,并且是有效的Lisp表达式。这就是DSL /数据子语言部分。


1
函数应用对我来说比线程有意义得多,但这肯定是一个习惯问题。好答案。
coredump
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.