Racket编程指南——15 反射和动态求值

15 反射和动态求值

Racket是一个动态(dynamic)的语言。它提供了许多用于加载、编译、甚至在运行时构造新代码的工具。

    15.1 eval

      15.1.1 本地域

      15.1.2 名称空间

      15.1.3 名称空间和模块

    15.2 操纵名称空间

      15.2.1 创建和安装名称空间

      15.2.2 跨名称空间共享数据和代码

    15.3 脚本求值和使用load

 

15.1 eval

这个例子在一个模块内或在DrRacket的定义窗口内将不会工作,但它会工作在交互窗口中,原因在《名称空间》的末尾讲解。

eval函数构成一个表达式或一个定义(如“引用(quoted)”表或语法对象(syntax object))的描述并且对它进行求值:

> (eval '(+ 1 2))

3

eval函数的强大在于可以动态构造一个表达式:

> (define (eval-formula formula)
    (eval `(let ([x 2]
                 [y 3])
             ,formula)))
> (eval-formula '(+ x y))

5

> (eval-formula '(+ (* x y) y))

9

当然,如果我们只是想用给出xy的值求值表达式,我们不需要eval。更直接的方法是使用一级函数:

> (define (apply-formula formula-proc)
    (formula-proc 2 3))
> (apply-formula (lambda (x y) (+ x y)))

5

> (apply-formula (lambda (x y) (+ (* x y) y)))

9

然而,如果像(+ x y)(+ (* x y) y)这样的表达式是从用户提供的文件中读取,那么eval可能是适当的。同样,REPL读取由用户输入的表达式,并使用eval求值。

一样地,在整个模块中eval经常直接或间接地被使用。例如,程序可以在定义域中用dynamic-require读取一个模块,这本质上是一个封装在eval中的动态加载模块的代码。

15.1.1 本地域

eval函数不能看到上下文中被调用的局部绑定。例如,调用在一个非引用的let表中的eval以对一个公式求值不会使得值xy可见:

> (define (broken-eval-formula formula)
    (let ([x 2]
          [y 3])
      (eval formula)))
> (broken-eval-formula '(+ x y))

x: undefined;

 cannot reference an identifier before its definition

  in module: top-level

eval函数不能看到xy的绑定,正是因为它是一个函数,并且Racket是词法作用域的语言。想象一下如果eval被实现为

(define (eval x)
  (eval-expanded (macro-expand x)))

那么在eval-expanded被调用的这个点上,x最近的绑定是表达式求值,不是broken-eval-formula中的let绑定。词法范围防止这样的困惑和脆弱的行为,从而防止eval表看到上下文中被调用的局部绑定。

你可以想象,即使通过eval不能看到broken-eval-formula中的局部绑定,这里实际上必须是一个x2y3的数据结构映射,以及你想办法得到那些数据结构。事实上,没有这样的数据结构存在;编译器可以自由地在编译时替换带有2x的每一个使用,因此在运行时的任何具体意义上都不存在x的局部绑定。即使变量不能通过常量折叠消除,通常也可以消除变量的名称,而保存局部值的数据结构与从名称到值的映射不一样。

15.1.2 名称空间

由于eval不能从它调用的上下文中看到绑定,另一种机制是需要确定动态可获得的绑定。一个名称空间(namespace)是一个一级的值,它封装了用于动态求值的可获得绑定。

通俗地说,术语名称空间(namespace)有时交替使用环境(environment)范围(scope)。在Racket里,术语名称空间(namespace)有更具体的、动态的意义,并且它不应该和静态的词汇概念混淆。

某些函数,如eval,接受一个可选的名称空间参数。通常,动态操作所使用的名称空间是current-namespace参数所确定的当前名称空间(current namespace)

evalREPL中使用时,当前名称空间是REPL使用于求值表达式中的一个。这就是为什么下面的互动设计成功通过eval访问x的原因:

> (define x 3)
> (eval 'x)

3

相反,尝试以下简单的模块并直接在DrRacket里或提供文件作为命令行参数给racket运行它:

#lang racket
(eval '(cons 1 2))

这个失败是因为初始的当前名称空间是空的。当你在交互模式下运行racket(见《交互模式》)时,初始的名称空间是用racket模块的导出初始化的,但是当你直接运行一个模块时,初始的名称空间开始为空。

在一般情况下,通过任何名称空间的安装来使用eval一个坏主意。相反,应明确地为调用而创建一个名称空间并安装它来求值:

#lang racket
(define ns (make-base-namespace))
(eval '(cons 1 2) ns) ; works

make-base-namespace函数创建一个名称空间,它是用racket/base的导出来初始化的。下一章节《操纵名称空间》提供了关于创建和配置名称空间的更多信息。

15.1.3 名称空间和模块

如同let绑定,词法范围意味着eval不能自动看到一个调用它的module(模块)的定义。然而,和let绑定不同的是,Racket提供了一种将模块反射到一个名称空间(namespace)的途径。

module->namespace函数接受一个引用的模块路径(module path),并生成一个名称空间,用于对表达式和定义求值,就像它们出现在module主体中一样:

> (module m racket/base
    (define x 11))
> (require 'm)
> (define ns (module->namespace ''m))
> (eval 'x ns)

11

''m中的双引号是因为'm是引用一个交互声明模块的模块路径,所以''m是路径的引用表。

module->namespace函数对来自于模块之外的模块是最有用的,在这里模块的全名是已知的。然而,在module表内,模块的全名可能并不知道,因为它可能取决于在最终加载时模块来源位于何处。

module内,使用define-namespace-anchor声明模块上的反射钩子,并使用namespace-anchor->namespace在模块的名称空间中滚动:

#lang racket
(define-namespace-anchor a)
(define ns (namespace-anchor->namespace a))
(define x 1)
(define y 2)
(eval '(cons x y) ns) ; produces (1 . 2)

 

15.2 操纵名称空间

一个名称空间(namespace)封装两条信息:

  • 从标识到绑定的一个映射。例如,一个名称空间可以将标识lambda映射到lambda表。一个“空”的名称空间是一个映射之一,它映射每个标识到一个未初始化的顶层变量。

  • 从模块名称到模块声明和实例的一个映射。

第一个映射是用于给一个顶层上下文中的表达式求值,如(eval '(lambda (x) (+ x 1)))中的。第二个映射是用于定位模块,例如通过dynamic-require。对(eval '(require racket/base))的调用通常使用两部分:标识映射确定require的绑定;如果它原来的意思是require,那么模块映射用于定位racket/base模块。

从核心Racket运行系统的角度来看,所有求值都是反射性的。执行从初始的名称空间包含一些原始的模块,并进一步由命令行上或在REPL提供指定加载的文件和模块。顶层require表和define表调整标识映射,模块声明(通常根据require表加载)调整模块映射。

15.2.1 创建和安装名称空间

函数make-empty-namespace创建一个新的空名称空间。由于名称空间确实是空的,所以它不能首先用来对任何顶级表达式求值——甚至不能对(require racket)求值。特别地,

(parameterize ([current-namespace (make-empty-namespace)])
  (namespace-require 'racket))

失败,因为名称空间不包括建立racket的原始模块。

为了使名称空间有用,必须从现有名称空间中附加一些模块。附加模块通过来自现有的名称空间映射的传递性复制条目(模块及它的所有导入)来调整模块名称的映射到实例。通常情况下,作为附加原始模块的替代——其名称和组织有可能会变化——附加一个高级模块,如racketracket/base

make-base-empty-namespace函数提供一个空的名称空间,除非附加了racket/base。从名称空间的绑定的标识部分没有映射的意义讲,生成的名称空间仍然是“空的”;只有模块映射已经填充。然而,通过初始的模块映射,可以加载更多模块。

一个用make-base-empty-namespace创建的名称空间适合于许多基本的动态任务。例如,假设my-dsl库实现了特定定义域的语言,你希望在其中执行来自用户指定文件的命令。一个用make-base-empty-namespace的名称空间足以启动:

(define (run-dsl file)
  (parameterize ([current-namespace (make-base-empty-namespace)])
    (namespace-require 'my-dsl)
    (load file)))

注意,current-namespaceparameterize不影响像在parameterize主体中的namespace-require那样的标识的含义。这些标识从封闭上下文(可能是一个模块)获得它们的含义。只有对代码具有动态性的表达式,如load的文件的内容,通过parameterize影响。

在上面的例子中,微妙的一点是使用(namespace-require 'my-dsl)代替(eval '(require my-dsl))。后者不会运行,因为eval需要对在名称空间中的require获得含义,并且名称空间的标识映射最初是空的。与此相反,namespace-require函数直接将给定的模块导入当前名称空间。从(namespace-require 'racket/base)运行。从(namespace-require 'racket/base)将为require引入绑定并使后续的(eval '(require my-dsl))运行。上面的更好,不仅仅是因为它更紧凑,还因为它避免了引入不属于特定领域语言的绑定。

15.2.2 跨名称空间共享数据和代码

如果模块不需要附加新的名称空间,则将重新加载并实例化它们。例如,racket/base不包括racket/class,加载racket/class又将创造一个不同的类数据类型:

> (require racket/class)
> (class? object%)

#t

> (class?
   (parameterize ([current-namespace (make-base-empty-namespace)])
     (namespace-require 'racket/class) ; loads again
     (eval 'object%)))

#f

对于动态加载的代码需要与其上下文共享更多代码和数据的情况,使用namespace-attach-module函数。 传递给namespace-attach-module的第一个参数是一个从中描绘模块实例的源名称空间;在某些情况下,当前名称空间对于包含需要共享的模块来说是已知的:

> (require racket/class)
> (class?
   (let ([ns (make-base-empty-namespace)])
     (namespace-attach-module (current-namespace)
                              'racket/class
                              ns)
     (parameterize ([current-namespace ns])
       (namespace-require 'racket/class) ; uses attached
       (eval 'object%))))

#t

然而,在一个模块中,define-namespace-anchornamespace-anchor->empty-namespace的组合提供了一种更可靠的获取源名称空间的方法:

#lang racket/base
(require racket/class)
(define-namespace-anchor a)
(define (load-plug-in file)
  (let ([ns (make-base-empty-namespace)])
    (namespace-attach-module (namespace-anchor->empty-namespace a)
                             'racket/class
                              ns)
    (parameterize ([current-namespace ns])
      (dynamic-require file 'plug-in%))))

namespace-attach-module绑定的锚将模块的运行时间与加载模块的名称空间(可能与当前名称空间不同)连接在一起。在上面的示例中,由于封闭模块需要racket/class,由namespace-anchor->empty-namespace生成的名称空间肯定包含了一个racket/class的实例。此外,该实例与导入模块的一个相同,从而类数据类型共享。

 

15.3 脚本求值和使用load

从历史上看,Lisp实现没有提供模块系统。相反,大的程序是由基本的脚本REPL来求值一个特定顺序的程序片段。虽然事实证明REPL脚本是构建程序和库的糟糕方法,但有时它仍然是一个有用的功能。

通过load用宏定义语言扩展[Flatt02]来描述程序交互性特别差。

load函数通过从文件中一个接一个地read(读取)S表达式并把它们传递给eval来运行一个REPL脚本。如果一个文件"place.rkts"包含以下内容

(define city "Salt Lake City")
(define state "Utah")
(printf "~a, ~a\n" city state)

那么,它可以加载进REPL

> (load "place.rkts")

Salt Lake City, Utah

> city

"Salt Lake City"

然而,由于load使用eval,像下面的模块一般不会运行——基于在《名称空间》中描述的相同原因:

#lang racket
(define there "Utopia")
(load "here.rkts")

对求值"here.rkts"的上下文的当前名称空间可能是空的;在任何情况下,你不能从"here.rkts"取得there。同样,在"here.rkts"里的任何定义对模块里的使用不会变得可见;毕竟,load是动态发生的,而在模块中标识的引用是从词法上解决,因此是静态的。

不像evalload不接受一个名称空间的参数。为了提供用于load的名称空间,设置current-namespace参数。下面的例子用racket/base模块的绑定对"here.rkts"中的表达式求值:

#lang racket
(parameterize ([current-namespace (make-base-namespace)])
  (load "here.rkts"))

你甚至可以用namespace-anchor->namespace来访问封闭模块的绑定以进行动态求值。在下面的例子中,当"here.rkts"被load(加载)时,它既可以指there,也可以指racket的绑定:

#lang racket
(define there "Utopia")
(define-namespace-anchor a)
(parameterize ([current-namespace (namespace-anchor->namespace a)])
  (load "here.rkts"))

不过,如果"here.rkts"定义任意的标识,这个定义不能在外围模块中直接(即静态地)引用。

racket/load模块语言不同于racketracket/base。一个模块使用racket/load动态对待其所有上下文,通过模块主体里的每一个表去eval(使用被racket初始化的名称空间)。因此,在模块主体中使用evalload可以看到直接主体表相同的动态名称空间。例如,如果"here.rkts"包含以下内容

(define here "Morporkia")
(define (go!) (set! here there))

那么运行

#lang racket/load
(define there "Utopia")
(load "here.rkts")
(go!)
(printf "~a\n" here)

打印“Utopia”。

使用racket/load的缺点包括减少错误检查、工具支持和性能。例如,以下程序

#lang racket/load
(define good 5)
(printf "running\n")
good
bad

DrRacket的语法检查(Check Syntax)工具不能告诉第二个good是对第一个的参考,而对bad的非绑定参考仅在运行时报告而不在语法上拒绝。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值