在Scheme中,语法对象的确切目的是什么?

7
我正在尝试用Python编写一个类似Scheme的小语言,以更好地理解Scheme。
问题在于我卡在了语法对象上。我无法实现它们,因为我不太明白它们的作用和工作原理。
为了尝试理解它们,我在DrRacket中玩弄了一下语法对象。
根据我所能找到的信息,评估“#'(+ 2 3)”与评估“'(+ 2 3)”没有任何区别,除非有一个词法“+”变量遮盖了顶层命名空间中的变量,在这种情况下,“(eval '(+ 2 3))”仍然返回“5”,但“(eval #'(+ 2 3))”会抛出错误。
例如:
(define (top-sym)
  '(+ 2 3))
(define (top-stx)
  #'(+ 2 3))
(define (shadow-sym)
  (define + *)
  '(+ 2 3))
(define (shadow-stx)
  (define + *)
  #'(+ 2 3))
(eval (top-sym))(eval (top-stx))(eval (shadow-sym)) 都返回 5,而 (eval (shadow-stx)) 则会抛出错误。它们都不会返回 6
如果我不了解的话,我会认为语法对象唯一特殊之处(除了存储代码位置以提供更好的错误报告这一微不足道的事实)是在某些情况下它们会抛出错误,而符号对应项可能会返回一个潜在的不想要的值。
如果事情如此简单,那么使用语法对象与使用常规列表和符号就没有什么真正的优势了。
所以我的问题是:我错过了语法对象的什么特点,使它们如此特别?

你看过这个吗:http://docs.racket-lang.org/reference/syntax-model.html#%28part._stxobj-model%29? - dyoo
@dyoo 我已经读了好几遍,这让我以为我理解了它们,直到我尝试了上面的测试。从那个页面上,我本来以为(eval (shadow-stx))应该返回6 - Matt
2
只是想澄清一下,当你询问有关语法对象时,那确实是Racket独有的东西:它并不是Scheme语言中普遍存在的东西。语法对象是Racket语言基础设施的核心数据类型。 - dyoo
1个回答

13

Syntax对象是底层Racket编译器的词法上下文存储库。具体来说,当我们输入以下程序时:

#lang racket/base
(* 3 4)

编译器接收一个表示整个程序内容的语法对象。下面是一个示例,让我们看一下该语法对象的样子:
#lang racket/base

(define example-program 
  (open-input-string
   "
    #lang racket/base
    (* 3 4)
   "))

(read-accept-reader #t)
(define thingy (read-syntax 'the-test-program example-program))
(print thingy) (newline)
(syntax? thingy)

请注意,在程序中,*thingy内部具有编译时表示为语法对象。目前,在thingy中的*还不知道它来自哪里:它还没有绑定信息。在编译期间的扩展过程中,编译器将*#lang racket/base中的*关联起来。
如果我们在编译时与这些内容进行交互,我们可以更轻松地看到这一点。(注意:我故意避免谈论eval,因为我想避免混淆关于编译时和运行时发生的事情的讨论。)
下面是一个示例,让我们更加了解这些语法对象的功能:
#lang racket/base
(require (for-syntax racket/base))

;; This macro is only meant to let us see what the compiler is dealing with
;; at compile time.

(define-syntax (at-compile-time stx)
  (syntax-case stx ()
    [(_ expr)
     (let ()
       (define the-expr #'expr)
       (printf "I see the expression is: ~s\n" the-expr)

       ;; Ultimately, as a macro, we must return back a rewrite of
       ;; the input.  Let's just return the expr:
       the-expr)]))


(at-compile-time (* 3 4))

我们将使用一个宏,在编译时,来让我们在编译期间检查事物的状态。如果您在DrRacket中运行此程序,您将看到DrRacket首先编译程序,然后运行它。编译程序时,当它遇到at-compile-time的用法时,编译器将调用我们的宏。
因此,在编译时,我们会看到类似这样的东西:
I see the expression is: #<syntax:20:17 (* 3 4)>

让我们稍微修改一下程序,看看是否可以检查标识符的identifier-binding

#lang racket/base
(require (for-syntax racket/base))

(define-syntax (at-compile-time stx)
  (syntax-case stx ()
    [(_ expr)
     (let ()
       (define the-expr #'expr)
       (printf "I see the expression is: ~s\n" the-expr)
       (when (identifier? the-expr)
         (printf "The identifier binding is: ~s\n" (identifier-binding the-expr)))

       the-expr)]))


((at-compile-time *) 3 4)

(let ([* +])
  ((at-compile-time *) 3 4))

如果我们在DrRacket中运行此程序,将会看到以下输出:
I see the expression is: #<syntax:21:18 *>
The identifier binding is: (#<module-path-index> * #<module-path-index> * 0 0 0)
I see the expression is: #<syntax:24:20 *>
The identifier binding is: lexical
12
7

(顺便说一句:为什么我们可以在编译时看到at-compile-time的输出呢?因为编译完全在运行时之前完成了!如果我们使用raco make预编译程序并保存字节码,那么当我们运行程序时就不会看到编译器被调用。)
当编译器到达at-compile-time的使用时,它知道将适当的词法绑定信息与标识符关联起来。当我们检查第一个案例中的identifier-binding时,编译器知道它与特定模块相关联(在这种情况下,是#lang racket/base,这就是module-path-index的作用)。但在第二种情况下,它知道它是一个词法绑定:编译器已经遍历了(let ([* +]) ...),所以它知道*的用法是指向由let设置的绑定。
Racket编译器使用语法对象将这种绑定信息传递给客户端,例如我们的宏。
尝试使用eval来检查这种东西存在许多问题:语法对象中的绑定信息可能不相关,因为在我们评估语法对象时,它们的绑定可能指向不存在的东西!这基本上就是你在实验中看到错误的原因。
尽管如此,这里有一个例子展示了s表达式和语法对象之间的区别:
#lang racket/base

(module mod1 racket/base
  (provide x)
  (define x #'(* 3 4)))

(module mod2 racket/base
  (define * +) ;; Override!
  (provide x)
  (define x  #'(* 3 4)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;

(require (prefix-in m1: (submod "." mod1))
         (prefix-in m2: (submod "." mod2)))

(displayln m1:x)
(displayln (syntax->datum m1:x))
(eval m1:x)

(displayln m2:x)
(displayln (syntax->datum m2:x))
(eval m2:x)

这个例子被精心构造,以便语法对象的内容只涉及模块绑定的事物,这些事物将在我们使用eval时存在。如果我们稍微改变一下这个例子,

(module broken-mod2 racket/base
  (provide x)
  (define x  
    (let ([* +])
      #'(* 3 4))))

当我们尝试evalbroken-mod2中出来的x时,事情就会出现严重的问题,因为语法对象正在引用一个在我们eval之前不存在的词法绑定。 eval是一种难以驾驭的东西。


顺便说一句,Matthew Flatt将在Clojure/West 2013上就这个主题进行演讲。他的演讲http://clojurewest.org/sessions#flatt将有更多例子,可以帮助澄清问题。 - dyoo
谢谢!这个演讲会有在线观看的吗? - Matt
@Matt:我想是这样,但我不是十分确定。Clojure/West 2012有一个由InfoQ托管的视频存档http://clojurewest.org/news/2012/5/11/clojurewest-video-schedule.html,所以我预计他们今年也会这样做。 - dyoo
那么,引用词法绑定的句法对象有什么用呢? - Matt
是的。一个具体的情况是在像Typed Racket这样的系统中,据我所知,必须跟踪所有绑定,包括词法和模块绑定,以便可以将类型与每个绑定位置关联起来。对这些绑定的词法引用可以通过像bound-identifier=?这样的比较与已知的绑定位置关联到它们的类型。解决您在语法对象和eval方面遇到的问题:这些问题是eval的问题,而不是语法对象的问题。Racket程序员尽量避免使用eval,除非他们真的知道自己在做什么。 - dyoo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接