7

假设我想在 s 表达式中的第一项以外的其他内容上触发 Scheme 宏。例如,假设我想define用中缀样式替换:=,这样:

(a := 5) -> (define a 5)
((square x) := (* x x)) -> (define (square x) (* x x))

实际的转变似乎很简单。诀窍是让 Scheme 找到:=表达式并对其进行宏扩展。我已经考虑过使用标准宏的中缀语法来包围大部分代码,也许是:(with-infix-define expr1 expr2 ...),并让标准宏遍历其主体中的表达式并执行任何必要的转换。我知道如果我采用这种方法,我将不得不小心避免转换实际上应该是数据的列表,例如引用列表和准引用列表的某些部分。我设想的一个例子:

(with-infix-define
   ((make-adder n) := (lambda (m) (+ n m)))

   ((foo) :=
       (add-3 := (make-adder 3))
       (add-6 := (make-adder 6))
       (let ((a 5) (b 6))
           (+ (add-3 a) (add-6 b))))

   (display (foo))
   (display '(This := should not be transformed))

所以,我的问题有两个:

  1. 如果我走这with-infix-define条路,除了报价和准报价外,我是否必须注意任何绊脚石?
  2. 我感觉有点像在重新发明轮子。这种类型的代码遍历似乎正是标准宏扩展系统必须要做的——唯一的区别是它们在决定是否进行任何代码转换时只查看列表中的第一项。有什么办法可以搭载现有系统吗?
4

2 回答 2

12
  1. 在继续此之前,最好仔细考虑一下——IME 您经常会发现您真正想要的读者级别的处理方式:=是中缀语法。这当然意味着它也是引号等中的中缀,所以现在看起来很糟糕,但我的经验是,你最终会意识到最好始终如一地做事。

  2. 为了完整起见,我会提到在 Racket 中有一个用于类似中缀的表达式的读取语法技巧:(x . define . 1)读取(define x 1). (如上所述,它无处不在。)

  3. 否则,您对包装宏的想法几乎是您唯一可以做的事情。但这并不意味着它完全没有希望,你可能有一个钩子到你的实现的扩展器中,它可以让你做这样的事情——例如,Racket 有一个特殊的宏#%module-begin,它包裹了一个完整的模块体并#%top-interaction包裹了顶层表达式REPL。(这两个都是在两个上下文中隐式添加的。)这是一个例子(为了简单起见,我使用 Racket define-syntax-rule):

    #lang racket/base
    
    (provide (except-out (all-from-out racket/base)
                         #%module-begin #%top-interaction)
             (rename-out [my-module-begin #%module-begin]
                         [my-top-interaction #%top-interaction]))
    
    (define-syntax infix-def
      (syntax-rules (:= begin)
        [(_ (begin E ...)) (begin (infix-def E) ...)]
        [(_ (x := E ...))  (define x (infix-def E) ...)]
        [(_ E)             E]))
    
    (define-syntax-rule (my-module-begin E ...)
      (#%module-begin (infix-def E) ...))
    (define-syntax-rule (my-top-interaction . E)
      (#%top-interaction . (infix-def E)))
    

    如果我把它放在一个名为 的文件中my-lang.rkt,我现在可以按如下方式使用它:

    #lang s-exp "my-lang.rkt"
    (x := 10)
    ((fib n) :=
     (done? := (<= n 1))
     (if done? n (+ (fib (- n 1)) (fib (- n 2)))))
    (fib x)
    
  4. 是的,你需要处理很多事情。上面的两个例子是处理begin表达式和处理函数体。这显然是一个非常部分的列表——您还需要 , 等的主体lambdalet但这仍然比一些盲目按摩要好,因为这不切实际,因为您无法真正提前知道一些随机代码是如何产生的将结束。作为一个简单的例子,考虑这个简单的宏:

    (define-syntax-rule (track E)
      (begin (eprintf "Evaluating: ~s\n" 'E)
             E))
    (x := 1)
    
  5. 这样做的结果是,为了获得适当的解决方案,您需要某种方法来预扩展代码,以便您可以扫描它并处理您的实现中为数不多的已知核心形式。

  6. 是的,所有这些都是重复宏扩展器所做的工作,但是由于您正在更改扩展的工作方式,所以没有办法解决这个问题。(要了解为什么它是一个根本性的变化,请考虑一下(if := 1)——这是一个条件表达式还是一个定义?你如何决定哪个优先?)因此,对于具有这种“可爱语法”的语言,一种更流行的方法就是将代码读取并解析成普通的 S 表达式,然后让实际的语言实现使用普通的函数和宏。

于 2012-05-29T05:27:37.190 回答
3

重新定义define有点复杂。请参阅@Eli 的出色解释。

另一方面,如果您满足:=于使用set!的东西会更简单一些。

这是一个小例子:

#lang racket

(module assignment racket
  (provide (rename-out [app #%app]))

  (define-syntax (app stx)
    (syntax-case stx (:=)
      [(_ id := expr)
       (identifier? #'id)
       (syntax/loc stx (set! id expr))]      
      [(_ . more) 
       (syntax/loc stx (#%app . more))])))

(require 'assignment)

(define x 41)
(x := (+ x 1))
(displayln x)

为了将示例保存在单个文件中,我使用了子模块(在 Racket 的预发布版本中可用)。

于 2012-05-29T09:46:56.217 回答