是否有可能/使用卫生宏进行编译时计算优化的示例是什么?



我一直在通读 https://lispcast.com/when-to-use-a-macro,它指出(关于clojure的宏)

另一个示例是在编译时执行昂贵的计算作为优化

我抬起头,似乎有不卫生的宏。这也可以应用于卫生吗?特别是谈到计划。据我了解卫生宏,它们只转换语法,但无论如何,代码的实际执行都会推迟到运行时。

是的。 宏卫生仅指宏扩展是否可以意外捕获标识符。 无论宏是否卫生,在编译时都会发生定期宏扩展(与读取器宏扩展相反)。 宏扩展将宏的代码替换为正在执行的结果。 它们的两个主要用例是转换语法(即DSL),通过消除运行时的计算或两者兼而有之来提高性能。

我想到了几个例子:

  1. 您更喜欢以度为单位的角度编写代码,但所有计算实际上都以弧度为单位。 您可以让宏在编译时消除这些琐碎但不必要的(在运行时)转换。
  2. 记忆是宏可用于计算优化的一个广泛示例。
  3. 您有一个表示 SQL 语句或复杂文本数学表达式的字符串,您希望解析甚至可能在编译时执行。

您还可以组合这些示例并拥有一个记忆SQL解析器。 几乎任何在编译时拥有所有必要输入并因此可以计算结果的情况都是候选的。

是的,卫生宏可以做这种事情。 举个例子,这里有一个在 Racket 中称为plus的宏,它与+类似,只是在宏展开时,它对相邻文字数字的序列求和。 因此,它执行了一些您可能期望在运行时宏扩展时(因此,有效地在编译时)完成的工作。 因此,例如

(plus a b 1 2 3 c 4 5)

扩展到

(+ a b 6 c 9)

关于此宏的一些注释。

  • 这可能不是很惯用的Racket,因为我是一个基本上没有改革的CL黑客,这意味着我住在一个山洞里,穿着动物皮,经常说"ug"。特别是我确信我应该使用syntax-parse但我无法理解它。
  • 它甚至可能不对。
  • 算术有一些微妙之处,这意味着这个宏可以返回与+不同的结果。 特别是+被定义为从左到右成对添加,而plus通常不是:所有文字都特别首先添加(假设你已经完成了(需要球拍/flonum,+max.0&c具有与它们在我的机器上相同的值),那么(+ -max.0 1.7976931348623157e+308 1.7976931348623157e+308)的值为1.7976931348623157e+308, 而(plus -max.0 1.7976931348623157e+308 1.7976931348623157e+308)的值为+inf.0,因为两个文字首先相加,这溢出了。
  • 一般来说,这是一件无用的事情:我认为,可以安全地假设任何合理的编译器都会为你做这些优化。 它的唯一目的是表明可以执行检测和编译编译时常量。
  • 值得注意的是,至少从像我这样的穴居人口齿舌兰用户的角度来看,你可以像对待+一样对待它,因为syntax-case中的最后一个:例如,它可以说(apply plus ...)(当然,在这种情况下没有发生聪明的优化)。

在这里:

(require (for-syntax racket/list))
(define-syntax (plus stx)
(define +/stx (datum->syntax stx +))
(syntax-case stx ()
[(_)
;; return additive identity
#'0]
[(_ a)
;; identity with one argument
#'a]
[(_ a ...)
;; the interesting case: there's more than one argument, so walk over them
;; looking for literal numbers.  This is probably overcomplicated and
;; unidiomatic
(let* ([syntaxes (syntax->list #'(a ...))]
[reduced (let rloop ([current (first syntaxes)]
[tail (rest syntaxes)]
[accum '()])
(cond
[(null? tail)
(reverse (cons current accum))]
[(and (number? (syntax-e current))
(number? (syntax-e (first tail))))
(rloop (datum->syntax stx
(+ (syntax-e current)
(syntax-e (first tail))))
(rest tail)
accum)]
[else
(rloop (first tail)
(rest tail)
(cons current accum))]))])
(if (= (length reduced) 1)
(first reduced)
;; make sure the operation is our +
#`(#,+/stx #,@reduced)))]
[_
;; plus on its own is +, but we want our one.  I am not sure this is right
+/stx]))
实际上,可以

更积极地做到这一点,以便将(plus a b 1 2 c 3)变成(+ a b c 6). 这可能具有更令人兴奋的可能得到不同答案的含义。 值得注意的是,CL 规范对此有何说明:

对于数学上是关联(并且可能是交换的)函数,符合要求的实现可以以与关联(可能和可能的交换)重排一致的任何方式处理参数。这不会影响参数表单的计算顺序 [...]。未指定的只是处理参数值的顺序。这意味着应用自动强制的实现可能有所不同[...]。

因此,像这样的优化在CL中显然是合法的:我不清楚它在Racket中是否合法(尽管我认为应该是合法的)。

(require (for-syntax racket/list))
(define-for-syntax (split-literals syntaxes)
;; split a list into literal numbers and the rest
(let sloop ([tail syntaxes]
[accum/lit '()]
[accum/nonlit '()])
(if (null? tail)
(values (reverse accum/lit) (reverse accum/nonlit))
(let ([current (first tail)])
(if (number? (syntax-e current))
(sloop (rest tail)
(cons (syntax-e current) accum/lit)
accum/nonlit)
(sloop (rest tail)
accum/lit
(cons current accum/nonlit)))))))
(define-syntax (plus stx)
(define +/stx (datum->syntax stx +))
(syntax-case stx ()
[(_)
;; return additive identity
#'0]
[(_ a)
;; identity with one argument
#'a]
[(_ a ...)
;; the interesting case: there's more than one argument: split the
;; arguments into literals and nonliterals and handle approprately
(let-values ([(literals nonliterals)
(split-literals (syntax->list #'(a ...)))])
(if (null? literals)
(if (null? nonliterals)
#'0
#`(#,+/stx #,@nonliterals))
(let ([sum/stx (datum->syntax stx (apply + literals))])
(if (null? nonliterals)
sum/stx
#`(#,+/stx #,@nonliterals #,sum/stx)))))]
[_
;; plus on its own is +, but we want our one.  I am not sure this is right
+/stx]))

最新更新