Racket編程指南——15 反射和動態求值

15 反射和動態求值

Racket是一個動態的語言。它提供了許多用於加載、編譯、甚至在運行時構造新代碼的工具。

    15.1 eval

      15.1.1 本地域

      15.1.2 命名空間(Namespace)

      15.1.3 命名空間和模塊

    15.2 操縱的命名空間

      15.2.1 創建和安裝命名空間

      15.2.2 共享數據和代碼的命名空間

    15.3 腳本求值和使用load


15.1 eval

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 undefined identifier

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 命名空間(Namespace)

由於eval不能從它調用的上下文中看到綁定,另一種機制是需要確定動態可獲得的綁定。一個命名空間(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(見《(part "start-interactive-mode")》)時,初始的命名空間是用racket模塊的導出初始化的,但是當你直接運行一個模塊時,初始的命名空間開始爲空。

在一般情況下,用任何命名空間安裝結果來使用eval一個壞主意。相反,明確地創建一個命名空間並安裝它以調用eval:

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

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

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) (+ x1)))中的。第二個映射是用於定位模塊,例如通過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的原始模塊。

爲了使命名空間有用,必須從現有命名空間中附加(attached)一些模塊。附加模塊通過從現有的命名空間的映射傳遞複製條目(模塊及它的所有導入)調整模塊名稱映射到實例。通常情況下,而不是僅僅附加原始模塊——其名稱和組織有可能發生變化——附加一個高級模塊,如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函數直接將給定的模塊導入(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函數通過從文件中一個接一個地read(讀取)S表達式來運行一個REPL腳本,並把它們傳遞給eval。如果一個文件"place.rkts"包含以下內容

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

那麼,它可以load(加載)進一個REPL

> (load "place.rkts")

Salt Lake City, Utah

> city

"Salt Lake City"

然而,由於load使用eval,像下面的一個模塊一般不會運行——基於命名空間(Namespace)(命名空間)中的相同原因描述:

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

對求值"here.rkts"的上下文的當前命名空間可能是空的;在任何情況下,你不能從"here.rkts"there(那裏)。同時,在"here.rkts"裏的任何定義對模塊裏的使用不會變得可見;畢竟,load是動態發生,而在模塊標識符引用是從詞法上解決,因此是靜態的。

不像evalload不接受一個命名空間的參數。爲了提供一個用於load的命名空間,設置current-namespace參數(parameter)。下面的示例求值在"here.rkts"中使用racket/base模塊綁定的表達式:

#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的非綁定參考僅在運行時報告而不是在語法上拒絕。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章