【问题标题】:Should macros have side effects?宏应该有副作用吗?
【发布时间】:2012-10-16 09:19:41
【问题描述】:

宏扩展可以(或应该)有副作用吗?例如,这里有一个宏,它实际上是在编译时抓取网页的内容:

#lang racket

(require (for-syntax net/url))
(require (for-syntax racket/port))

(define-syntax foo
  (lambda (syntx)
    (datum->syntax #'lex
                   (port->string
                     (get-pure-port
                       (string->url
                         (car (cdr (syntax->datum syntx)))))))))

然后,我可以做(foo "http://www.pointlesssites.com/"),它将被替换为"\r\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"\r\n\t <and so on>"

这是一种好习惯吗?我是否保证 Racket 只会运行此代码一次?如果我在宏中添加(display "running...") 行,它只会打印一次,但我不想从一个例子中概括...

PS - 我问的原因是因为我实际上认为这有时真的很有用。例如this 是一个库,它允许您(在编译时)从 Google API 发现服务加载发现文档并自动为其创建包装器。我认为如果库实际上是从网络而不是从本地文件中获取发现文档,那将是非常酷的。

另外,举一个具有不同副作用的宏的示例:我曾经构建了一个宏,它将 Racket 的一个小子集转换为(eta-expanded)lambda 演算(当然,它仍然可以在球拍)。每当宏完成函数的翻译,它会将结果存储在字典中,以便以后调用宏可以在自己的翻译中使用该函数定义。

【问题讨论】:

  • 好的,笨蛋。我刚刚意识到我链接到的文档发现库是在编译时读取文件,这是另一种形式的副作用。这并不一定意味着这是一件好事,但它是在真实的库中完成的,所以......
  • 关于什么是“副作用”,我最喜欢你记忆结果的例子。 (与读取本地或远程文件的示例相反。在编译时读取 JSON 发现文档与读取 .RKT 文件并没有太大区别。事实上,我几乎朝着发现文档的方向前进,除了我希望他们按原样工作,而不是需要添加 #lang 行。但它可以这样工作的事实——JSON 发现文档可能是一种“语言”——是一个非常球拍很酷。)
  • 我很喜欢。问一个涉及图书馆的问题,该图书馆的作者随便给你留言:)我仍然只是学习 Racket 的所有语言扩展/构建功能,但我学的越多,我就越惊讶。

标签: macros racket side-effects


【解决方案1】:

简短的回答

宏有副作用是可以的,但你应该确保你的程序在提前编译时不会改变行为。

更长的答案

带有副作用的宏是一个强大的工具,它可以让你做一些让程序更容易编写的事情,或者实现一些根本不可能的事情。但是在宏中使用副作用时需要注意一些陷阱。幸运的是,Racket 提供了所有工具来确保您可以正确执行此操作。

最简单的宏副作用是您使用一些外部状态来查找要生成的代码。您在问题中列出的示例(阅读 Google API 描述)就是这种类型。一个更简单的例子是include 宏:

#lang racket
(include "my-file.rktl")

这会读取myfile.rktl 的内容并将其放在使用include 表单的位置。

现在,include 不是构建程序的好方法,但这在宏中是一种非常良性的副作用。如果你提前编译文件,它的工作原理和不编译一样,因为include 的结果是文件的一部分。

另一个不好的简单例子是这样的:

#lang racket
(define-syntax (show-file stx)
  (printf "using file ~a\n" (syntax-source stx))
  #'(void))

(show-file)

这是因为 printf 仅在编译时执行,因此如果您提前编译使用 show-file 的程序(与 raco make 一样),那么 printf 将会发生,并且不会在程序运行时发生,这可能不是本意。

幸运的是,Racket 有一种技术可以让你有效地编写像show-file 这样的宏。基本思想是留下实际执行副作用的剩余代码。特别是,您可以为此目的使用 Racket 的begin-for-syntax 表单。这是我要写的show-file

#lang racket
(define-syntax (show-file stx)
  #`(begin-for-syntax
      (printf "using file ~a\n" #,(syntax-source stx))))

(show-file)

现在,printf 不是在 show-file 宏被扩展时发生,而是在 show-file 生成的代码中发生,源代码嵌入在扩展的语法。这样,您的程序在提前编译的情况下仍能正常工作。

宏的其他用途也有副作用。 Racket 中最突出的一项是模块间通信——因为require 不会产生所需模块可以获得的值,因此在模块之间进行通信最有效的方法是使用副作用。要在存在编译的情况下进行这项工作,需要与 begin-for-syntax 几乎完全相同的技巧。

这是 Racket 社区,尤其是我思考过很多的话题,并且有几篇学术论文讨论了它是如何工作的:

Composable and Compilable Macros: You want it when?,马修·弗拉特,ICFP 2002

Advanced Macrology and the Implementation of Typed Scheme、Ryan Culpepper、Sam Tobin-Hochstadt 和 Matthew Flatt,2007 年方案研讨会

Languages as Libraries、Sam Tobin-Hochstadt、Ryan Culpepper、Vincent St-Amour、Matthew Flatt 和 Matthias Felleisen,PLDI 2011

【讨论】:

【解决方案2】:

在常见的 Lisp 中,函数 eval-when 允许您决定何时扩展宏。

【讨论】:

    猜你喜欢
    • 2010-11-29
    • 2019-03-25
    • 2010-11-27
    • 1970-01-01
    • 2013-11-03
    • 2019-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多