我正在尝试用 python 编写一种类似方案的小型语言,以便更好地理解方案。
问题是我被语法对象困住了。我无法实现它们,因为我不太了解它们的用途和工作原理。
为了尝试理解它们,我在 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
。
如果我不太了解的话,我会认为语法对象的唯一特殊之处(除了它们存储代码位置以更好地报告错误的小事实之外)是它们在某些情况下抛出错误在这种情况下,它们的符号对应项将返回可能不需要的值。
如果故事这么简单,那么使用语法对象相对于常规列表和符号就没有真正的优势。
所以我的问题是:对于语法对象,我缺少什么使得它们如此特别?
最佳答案
语法对象是底层 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
中的 *
不知道它来自哪里:它还没有绑定(bind)信息。正是在编译的扩展过程中,编译器将*
关联为对#langracket/的
。*
的引用基础
如果我们在编译时与事物交互,我们可以更容易地看到这一点。 (注意:我故意避免谈论 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))
我们将在这里使用一个宏,at-compile-time
,让我们在编译期间检查事物的状态。如果你在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
时,它知道将适当的词法绑定(bind)信息与标识符相关联。当我们在第一种情况下检查标识符绑定(bind)时,编译器知道它与特定模块关联(在本例中为#langracket/base,这就是module-path-index
业务是这样的)。但在第二种情况下,它知道这是一个词法绑定(bind):编译器已经遍历了(let ([* +]) ...)
,所以它知道 *
的使用引用了 let
设置的绑定(bind)。
Racket 编译器使用语法对象将这种绑定(bind)信息传递给客户端,例如我们的宏。
<小时/>尝试使用 eval
来检查此类内容充满了问题:语法对象中的绑定(bind)信息可能不相关,因为当我们评估语法对象时,它们的绑定(bind)可能会发生变化。指不存在的事物!这就是您在实验中看到错误的根本原因。
不过,这里有一个示例,显示了 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)
这个示例经过精心构建,以便语法对象的内容仅引用模块绑定(bind)的内容,这些内容将在我们使用 eval
时存在。如果我们稍微改变一下这个例子,
(module broken-mod2 racket/base
(provide x)
(define x
(let ([* +])
#'(* 3 4))))
然后,当我们尝试eval
来自broken-mod2
的x
时,事情会严重崩溃,因为语法对象引用的是当我们eval
时,词法绑定(bind)还不存在。 eval
是一头困难的野兽。
关于scheme - 方案中语法对象的确切用途是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15451852/