宏应该有副作用吗?

Ord*_*Ord 12 macros side-effects racket

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

#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)))))))))
Run Code Online (Sandbox Code Playgroud)

然后,我可以做(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 - 我问的原因是因为我实际上认为这有时候非常有用.例如,是一个库,允许您从Google API Discovery服务加载(在编译时)发现文档,并自动为其创建包装器.我认为,如果库实际上是从Web获取发现文档而不是本地文件,那将会非常酷.

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

Sam*_*adt 13

简短的回答

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

答案越长

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

最简单的宏副作用是使用某种外部状态来查找要生成的代码.您在问题中列出的示例(阅读Google API说明)就是这种情况.一个更简单的例子是include宏:

#lang racket
(include "my-file.rktl")
Run Code Online (Sandbox Code Playgroud)

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

现在,include这不是构建程序的好方法,但这在宏中是一种非常良性的副作用.如果你提前编译文件就好像你没有那样,因为它的结果include是文件的一部分.

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

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

(show-file)
Run Code Online (Sandbox Code Playgroud)

这是因为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)
Run Code Online (Sandbox Code Playgroud)

现在,而不是当发生show-file膨胀时,printf发生在该代码show-file 生成,具有嵌入在扩展语法的来源.这样,您的程序可以在提前编译的情况下继续正常工作.

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

这是Racket社区,特别是我对此有很多想法的话题,有几篇学术论文在讨论它是如何工作的:

可组合和可编译宏:您何时需要它?,Matthew Flatt,ICFP 2002

高级宏观学和类型计划的实施,Ryan Culpepper,Sam Tobin-Hochstadt和Matthew Flatt,2007年计划研讨会

作为图书馆的语言,Sam Tobin-Hochstadt,Ryan Culpepper,Vincent St-Amour,Matthew Flatt和Matthias Felleisen,PLDI 2011