由关键字触发的Scheme宏,不是列表的头部

时间:2012-05-28 23:33:14

标签: macros lisp scheme

假设我想在s-expression中第一项之外的其他内容上触发Scheme宏。例如,假设我想将define替换为中缀式:=,以便:

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

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

(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. 我觉得我有点像重新发明轮子。这种类型的代码行走似乎完全标准的宏扩展系统必须做什么 - 唯一的区别是它们在决定是否进行任何代码转换时只查看列表中的第一项。有什么方法可以搭载现有的系统吗?

2 个答案:

答案 0 :(得分: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. 这样做的结果是,对于正确的解决方案,您需要一些方法来预扩展代码,以便您可以扫描它并处理您的implmenetation中的几个已知核心形式。

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

答案 1 :(得分: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的预发布版本中可用)。