宏的目的是什么?

6
这里我主要关注Scala和Lisp / Scheme宏,而不是C / C ++ / Obj-C中的宏。
我只是看不到它的意义。
我理解的方式是,宏存在是为了扩展语言。但函数也是如此。
我理解有些东西由于语言限制不能干净地实现,因此宏需要是正确的方法。但是,我看到的许多宏示例似乎是使用普通函数可以相当简单实现的事情。
那么究竟是什么目的?无论是清洁的宏还是其他宏,请有人给我启示。如果可能,提供一些可以在宏中完成但使用普通函数不可能/难以完成的示例代码。

https://dev59.com/5nVC5IYBdhLWcg3whRcw?s=6|1.8097 https://dev59.com/F2025IYBdhLWcg3wvIgo#5833388?s=11|1.6415#5833388 - Rainer Joswig
Lisp宏的用途示例 - Rainer Joswig
3个回答

7
这里有一个简洁的总结,介绍了Scala宏的应用: http://scalamacros.org/paperstalks/2014-02-04-WhatAreMacrosGoodFor.pdf。简单来说,宏通常用于以下三个方面:1)代码生成,2)高级静态检查,3)增强领域特定语言的能力。在Scala中,基于类型的宏提供了额外而强大的功能,这是Lisp等语言中宏通常不具备的。
上述幻灯片中的一些示例确实可以不使用宏来实现,但结果会在某些方面存在缺陷(例如性能)或对用户过于复杂(例如因为繁重的错误消息)。例如,Akka的类型通道可能可以通过纯隐式实现,但编译速度和可理解性会受到影响。或者,scala/async可以作为编译器插件实现,但那样将不得不依赖于内部编译器API,并且更难以分发。
当然,宏并非万能药。显然,有些情况下它们并不是最佳选择,这在http://scalamacros.org/paperstalks/2014-03-01-MacrosVsTypes.pdf中有所说明。然而,值得注意的是,在某些情况下,既不是纯宏方案,也不是无宏方案,而是精心构建的混合方案最终表现最佳。

第一次演示中有几个例子。我认为1、2、5、7和8没有宏是无法实现的。3和4可以通过高级类型级编程完成,但结果在编译时和运行时都会变慢,并且维护起来更加困难。6可以近似实现,但用户体验会受到影响(错误消息、不一致的词汇)。 - Eugene Burmako

2

在Common Lisp和Scheme中,大部分特殊语法都是通过宏来实现的。例如,Scheme和CL都有if、cond和case,但只有if是原始语法。

标准定义的宏和自己制作的宏没有什么特别之处。它们可以像原语一样良好地运行和表现。

使用宏的代价是会让读者感到难以理解和惊讶。使用常见的命名约定,如with-*,可能会有所帮助,但是如果一个函数/过程可以完成工作或者表单只会在少数地方使用,就不应该使用宏。


1
在Lisp中(不确定Scala),宏主要用于以下三个目的:
  • 定义:创建某些内容并直接注册到适当的位置。例如: defun, defgeneric, defclass(标准库),deftable(postmodern)。
  • Unwind-protect包装器:暂时修改状态,并确保在任务完成后将其修改回来。这样重复编写很麻烦,因此我们创建了一个简写。例如:with-open-file(标准库),with-transaction(许多数据库库)。
  • 生成其他语言:例如CL-WHO(HTML),Parenscript(JavaScript)。通过在Lisp形式中生成其他语言的代码,即使它们本身不支持该功能,我们也可以为这些其他语言使用宏。

具体例子:Java 7引入了一种简写方式,以确保在try块中关闭Closable

try (SomeClosable foo = openFoo()) {
    foo.doSomething();
}

在Java 6中,只能大致地这样表达:
SomeClosable foo;
try {
    foo = openFoo();
    foo.doSomething();
} finally {
    if (foo != null && foo.isOpen()) {
        foo.close();
    }
}

Java开发者需要等待语言设计师实现此功能。而Lisp开发者则使用一个小的宏:

(defmacro with-open-foo ((var &rest options) &body body)
  `(let ((,var (open-foo ,@options)))
     (unwind-protect
         (progn ,@body)
       (when ,var (close ,var)))))

这样他就可以写作了

(with-open-foo (f :bar baz)
  (do-some-foo f)
  (and-something-else))

替代

(let ((f (open-foo :bar baz)))
  (unwind-protect
      (progn
        (do-some-foo f)
        (and-something-else))
    (when f (close f))))

你能否使用 defunwith-open-foo 写成一个函数呢? - Electric Coffee
@ElectricCoffee:简单的回答是:不行。更详细的回答是:你可以使用回调函数来实现类似的功能,但编写起来更加复杂,使用起来更加繁琐,并且需要额外调用两个函数。 - Svante
抱歉重新唤起一个已经过时的话题,但我仍然不明白你的示例如何从代码生成中受益,并且不能仅使用函数调用来实现。我的意思是为什么在这种情况下变量参数的函数不起作用?你能展示一下它如何使用起来很麻烦吗? - Electric Coffee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接