天天看點

Racket程式設計指南——14 單元 (元件)

14 單元 (元件)

單元(unit)組織程式分成獨立的編譯和可重用的元件(component)。一個單元類似于過程,因為這兩個都是用于抽象的一級值。雖然程式對表達式中的值進行抽象,但在集合定義中對名稱進行抽象。正如一個過程被調用來對它的表達式求值,表達式把實際的參數作為給它的正式參數,一個單元被調用(invoked)來對它的定義求值,這個定義給出其導入變量的實際引用。但是,與過程不同的是,在調用之前,一個單元的導入變量可以部分地與另一個之前調用(prior to invocation)單元的導出變量連結。連結将多個單元合并成單個複合單元。複合單元本身導入将傳播到連結單元中未解決的導入變量的變量,并從連結單元中重新導出一些變量以進一步連結。

    14.1  簽名和單元
    14.2 調用單元
    14.3 連結單元
    14.4 一級單元
    14.5 完整的module簽名和單元
    14.6 單元合約
      14.6.1 給簽名添加合約
      14.6.2 給單元添加合約
    14.7 unit(單元)與module(子產品)的比較

14.1  簽名和單元

單元的接口用簽名(signature)來描述。每個簽名都使用define-signature來定義(通常在module(子產品)中)。例如,下面的簽名,放在一個"toy-factory-sig.rkt"的檔案中,描述了一個元件的導出(export),它實作了一個玩具廠(toy factory):

"toy-factory-sig.rkt"
#lang racket
(define-signature toy-factory^
  (build-toys  ; (integer? -> (listof toy?))
   repaint     ; (toy? symbol? -> toy?)
   toy?        ; (any/c -> boolean?)
   toy-color)) ; (toy? -> symbol?)
(provide toy-factory^)

一個toy-factory^簽名的實作是用define-unit來寫的,它定義了一個名為toy-factory^的export(導出)從句:

"simple-factory-unit.rkt"
#lang racket
(require "toy-factory-sig.rkt")
(define-unit simple-factory@
  (import)
  (export toy-factory^)
  (printf "Factory started.\n")
  (define-struct toy (color) #:transparent)
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy 'blue)))
  (define (repaint t col)
    (make-toy col)))
(provide simple-factory@)

toy-factory^簽名也可以被一個單元引用,它需要一個玩具工廠來實施其它某些東西。在這種情況下,toy-factory^将以一個import(導入)從句命名。例如,玩具店可以從玩具廠買到玩具。(假設為了一個有趣的例子,商店隻願意出售特定顔色的玩具)。

"toy-store-sig.rkt"
#lang racket
(define-signature toy-store^
  (store-color     ; (-> symbol?)
   stock!          ; (integer? -> void?)
   get-inventory)) ; (-> (listof toy?))
(provide toy-store^)
"toy-store-unit.rkt"
#lang racket
(require "toy-store-sig.rkt"
         "toy-factory-sig.rkt")
(define-unit toy-store@
  (import toy-factory^)
  (export toy-store^)
  (define inventory null)
  (define (store-color) 'green)
  (define (maybe-repaint t)
    (if (eq? (toy-color t) (store-color))
        t
        (repaint t (store-color))))
  (define (stock! n)
    (set! inventory
          (append inventory
                  (map maybe-repaint
                       (build-toys n)))))
  (define (get-inventory) inventory))
(provide toy-store@)

請注意,"toy-store-unit.rkt"導入"toy-factory-sig.rkt",而不是"simple-factory-unit.rkt"。是以,toy-store@單元隻依賴于玩具工廠的規格,而不是具體的實施。

14.2 調用單元

simple-factory@單元沒有導入,是以可以使用invoke-unit直接調用它:

> (require "simple-factory-unit.rkt")
> (invoke-unit simple-factory@)
Factory started.

但是,invoke-unit表并不能使主體定義可用,是以我們不能在這家工廠制造任何玩具。define-values/invoke-unit表将簽名的辨別符綁定到實作簽名的一個單元(要調用的)提供的值:

> (define-values/invoke-unit/infer simple-factory@)
Factory started.
> (build-toys 3)
(list (toy 'blue) (toy 'blue) (toy 'blue))

由于simple-factory@導出toy-factory^簽名,toy-factory^的每個辨別符都是由define-values/invoke-unit/infer表定義的。表名稱的/infer部分表明,由聲明限制的辨別符是從simple-factory@推斷出來的。

在定義toy-factory^的辨別後,我們還可以調用toy-store@,它導入toy-factory^以産生toy-store^:

> (require "toy-store-unit.rkt")
> (define-values/invoke-unit/infer toy-store@)
> (get-inventory)
'()
> (stock! 2)
> (get-inventory)
(list (toy 'green) (toy 'green))

同樣,/infer部分define-values/invoke-unit/infer确定toy-store@導入toy-factory^,是以它提供與toy-factory^中的名稱比對的頂級綁定,如導入toy-store@。

14.3 連結單元

我們可以借助玩具工廠的合作使我們的玩具店玩具經濟性更有效,不需要重新建立。相反,玩具總是使用商店的顔色來制造,而工廠的顔色是通過導入toy-store^來獲得的:

"store-specific-factory-unit.rkt"
#lang racket
(require "toy-store-sig.rkt"
         "toy-factory-sig.rkt")
(define-unit store-specific-factory@
  (import toy-store^)
  (export toy-factory^)
  (define-struct toy () #:transparent)
  (define (toy-color t) (store-color))
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy)))
  (define (repaint t col)
    (error "cannot repaint")))
(provide store-specific-factory@)

要調用store-specific-factory@,我們需要toy-store^綁定供及給單元。但是為了通過調用toy-store^來獲得toy-store^的綁定,我們需要一個玩具工廠!單元實作是互相依賴的,我們不能在另一個之前調用那一個。

解決方案是将這些單元連結(link)在一起,然後調用組合單元。define-compound-unit/infer表将任意數量的單元連結成一個組合單元。它可以從相連的單元中進行導入和導出,并利用其它連結單元的導出來滿足各單元的導入。

> (require "toy-factory-sig.rkt")
> (require "toy-store-sig.rkt")
> (require "store-specific-factory-unit.rkt")
> (define-compound-unit/infer toy-store+factory@
    (import)
    (export toy-factory^ toy-store^)
    (link store-specific-factory@
          toy-store@))

上邊總的結果是一個單元toy-store+factory@,其導出既是toy-factory^也是toy-store^。從每個導入和導出的簽名中推斷出store-specific-factory@和toy-store@之間的聯系。

這個單元沒有導入,是以我們可以随時調用它:

> (define-values/invoke-unit/infer toy-store+factory@)
> (stock! 2)
> (get-inventory)
(list (toy) (toy))
> (map toy-color (get-inventory))
'(green green)

14.4 一級單元

define-unit表将define與unit表相結合,類似于(define (f x) ....)結合define,後跟帶一個隐式的lambda的辨別符。

擴大簡寫,toy-store@的定義幾乎可以寫成

(define toy-store@
  (unit
   (import toy-factory^)
   (export toy-store^)
   (define inventory null)
   (define (store-color) 'green)
   ....))

這個擴充和define-unit的差別在于,toy-store@的導入和導出不能被推斷出來。也就是說,除了将define和unit結合在一起,define-unit還将靜态資訊附加到定義的辨別符,以便靜态地提供它的簽名資訊來define-values/invoke-unit/infer和其它表。

雖有丢失靜态簽名資訊的缺點,unit可以與使用第一類值的其它表結合使用。例如,我們可以封裝一個unit,它在一個 lambda中建立一個玩具商店來提供商店的顔色:

"toy-store-maker.rkt"
#lang racket
(require "toy-store-sig.rkt"
         "toy-factory-sig.rkt")
(define [email protected]
  (lambda (the-color)
    (unit
     (import toy-factory^)
     (export toy-store^)
     (define inventory null)
     (define (store-color) the-color)
     ; the rest is the same as before
     (define (maybe-repaint t)
       (if (eq? (toy-color t) (store-color))
           t
           (repaint t (store-color))))
     (define (stock! n)
       (set! inventory
             (append inventory
                     (map maybe-repaint
                          (build-toys n)))))
     (define (get-inventory) inventory))))
(provide [email protected])

要調用由[email protected]建立的單元,我們必須使用define-values/invoke-unit,而不是/infer變量:

> (require "simple-factory-unit.rkt")
> (define-values/invoke-unit/infer simple-factory@)
Factory started.
> (require "toy-store-maker.rkt")
> (define-values/invoke-unit ([email protected] 'purple)
    (import toy-factory^)
    (export toy-store^))
> (stock! 2)
> (get-inventory)
(list (toy 'purple) (toy 'purple))

在define-values/invoke-unit表中,(import toy-factory^)行從目前的上下文中擷取與toy-factory^中的名稱比對的綁定(我們通過調用simple-factory@)建立的名稱),并将它們提供于導入toy-store@。(export toy-store^)從句表明[email protected]産生的單元将導出toy-store^,并在調用該單元後定義該簽名的名稱。

為了把一個單元與[email protected]連結起來,我們可以使用compound-unit表:

> (require "store-specific-factory-unit.rkt")
> (define toy-store+factory@
    (compound-unit
     (import)
     (export TF TS)
     (link [((TF : toy-factory^)) store-specific-factory@ TS]
           [((TS : toy-store^)) toy-store@ TF])))

這個compound-unit表将許多資訊聚集到一個地方。link從句中的左側TF和TS是綁定辨別符。辨別符TF基本上綁定到toy-factory^的元素作為由store-specific-factory@的實作。辨別符TS類似地綁定到toy-store^的元素作為由toy-store@的實作。同時,綁定到TS的元素作為提供給store-specific-factory@的導入,因為TS是随着store-specific-factory@的。綁定到TF的元素也同樣提供給toy-store^。最後,(export TF TS)表明綁定到TF和TS的元素從複合單元導出。

上面的compound-unit表使用store-specific-factory@作為一個一級單元,盡管它的資訊可以推斷。除了在推理上下文中的使用外,每個單元都可以用作一個一級單元。此外,各種表讓程式員彌合了推斷的和一級的世界之間的間隔。例如,define-unit-binding将一個新的辨別符綁定到由任意表達式生成的單元;它靜态地将簽名資訊與辨別符相關聯,并動态地對表達式産生的一級單元進行簽名檢查。

14.5 完整的module簽名和單元

在程式中使用的單元,子產品如"toy-factory-sig.rkt"和"simple-factory-unit.rkt"是常見的。racket/signature和racket/unit子產品的名稱可以作為語言來避免大量的樣闆子產品、簽名和單元申明文本。

例如,"toy-factory-sig.rkt"可以寫為

#lang racket/signature
build-toys  ; (integer? -> (listof toy?))
repaint     ; (toy? symbol? -> toy?)
toy?        ; (any/c -> boolean?)
toy-color   ; (toy? -> symbol?)

簽名toy-factory^是自動從子產品中提供的,它通過用^從檔案名"toy-factory-sig.rkt"置換"-sig.rkt"字尾來推斷。

同樣,"simple-factory-unit.rkt"子產品可以寫為

#lang racket/unit
(require "toy-factory-sig.rkt")
(import)
(export toy-factory^)
(printf "Factory started.\n")
(define-struct toy (color) #:transparent)
(define (build-toys n)
  (for/list ([i (in-range n)])
    (make-toy 'blue)))
(define (repaint t col)
  (make-toy col))

單元simple-factory@是自動從子產品中提供,它通過用@從檔案名"simple-factory-unit.rkt"置換"-unit.rkt"字尾來推斷。

14.6 單元合約

有兩種用合約保護單元的方法。一種方法在編寫新的簽名時是有用的,另一種方法當一個單元必須符合已經存在的簽名時就可以處理這種情況。

14.6.1 給簽名添加合約

當合約添加到簽名時,實作該簽名的所有單元都受到這些合約的保護。toy-factory^簽名的以下版本添加了前面說明中寫過的合約:

"contracted-toy-factory-sig.rkt"
#lang racket
(define-signature contracted-toy-factory^
  ((contracted
    [build-toys (-> integer? (listof toy?))]
    [repaint    (-> toy? symbol? toy?)]
    [toy?       (-> any/c boolean?)]
    [toy-color  (-> toy? symbol?)])))
(provide contracted-toy-factory^)

現在我們采用以前實作的simple-factory@,并實作toy-factory^的這個版本來代替:

"contracted-simple-factory-unit.rkt"
#lang racket
(require "contracted-toy-factory-sig.rkt")
(define-unit contracted-simple-factory@
  (import)
  (export contracted-toy-factory^)
  (printf "Factory started.\n")
  (define-struct toy (color) #:transparent)
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy 'blue)))
  (define (repaint t col)
    (make-toy col)))
(provide contracted-simple-factory@)

和以前一樣,我們可以調用我們的新單元并綁定導出,這樣我們就可以使用它們。然而這次,濫用導出引起相應的合約錯誤。

> (require "contracted-simple-factory-unit.rkt")
> (define-values/invoke-unit/infer contracted-simple-factory@)
Factory started.
> (build-toys 3)
(list (toy 'blue) (toy 'blue) (toy 'blue))
> (build-toys #f)
build-toys: contract violation
  expected: integer?
  given: #f
  in: the 1st argument of
      (-> integer? (listof toy?))
  contract from:
      (unit contracted-simple-factory@)
  blaming: top-level
   (assuming the contract is correct)
  at: eval:34.0
> (repaint 3 'blue)
repaint: contract violation
  expected: toy?
  given: 3
  in: the 1st argument of
      (-> toy? symbol? toy?)
  contract from:
      (unit contracted-simple-factory@)
  blaming: top-level
   (assuming the contract is correct)
  at: eval:34.0
14.6.2 給單元添加合約

然而,有時我們可能有一個單元,它必須符合一個已經存在的簽名而不是符合合約。在這種情況下,我們可以建立一個帶unit/c或使用define-unit/contract表的單元合約,它定義了一個已被單元合約包裝的單元。

例如,這裡有一個toy-factory@的版本,它仍然實作了規則toy-factory^,但它的輸出得到了适當的合約的保護。

"wrapped-simple-factory-unit.rkt"
#lang racket
(require "toy-factory-sig.rkt")
(define-unit/contract wrapped-simple-factory@
  (import)
  (export (toy-factory^
           [build-toys (-> integer? (listof toy?))]
           [repaint    (-> toy? symbol? toy?)]
           [toy?       (-> any/c boolean?)]
           [toy-color  (-> toy? symbol?)]))
  (printf "Factory started.\n")
  (define-struct toy (color) #:transparent)
  (define (build-toys n)
    (for/list ([i (in-range n)])
      (make-toy 'blue)))
  (define (repaint t col)
    (make-toy col)))
(provide wrapped-simple-factory@)
> (require "wrapped-simple-factory-unit.rkt")
> (define-values/invoke-unit/infer wrapped-simple-factory@)
Factory started.
> (build-toys 3)
(list (toy 'blue) (toy 'blue) (toy 'blue))
> (build-toys #f)
wrapped-simple-factory@: contract violation
  expected: integer?
  given: #f
  in: the 1st argument of
      (unit/c
       (import)
       (export (toy-factory^
                (build-toys
                 (-> integer? (listof toy?)))
                (repaint (-> toy? symbol? toy?))
                (toy? (-> any/c boolean?))
                (toy-color (-> toy? symbol?))))
       (init-depend))
  contract from:
      (unit wrapped-simple-factory@)
  blaming: top-level
   (assuming the contract is correct)
  at: <collects>/racket/unit.rkt
> (repaint 3 'blue)
wrapped-simple-factory@: contract violation
  expected: toy?
  given: 3
  in: the 1st argument of
      (unit/c
       (import)
       (export (toy-factory^
                (build-toys
                 (-> integer? (listof toy?)))
                (repaint (-> toy? symbol? toy?))
                (toy? (-> any/c boolean?))
                (toy-color (-> toy? symbol?))))
       (init-depend))
  contract from:
      (unit wrapped-simple-factory@)
  blaming: top-level
   (assuming the contract is correct)
  at: <collects>/racket/unit.rkt

14.7 unit(單元)與module(子產品)的比較

作為子產品的一個表,unit(單元)是對module(子產品)的補充:

  • module表主要用于管理通用命名空間。例如,它允許一個代碼片段是專指來自racket/base的car運算——其中一個提取内置配對資料類型的一個執行個體的第一個元素——而不是任何其它帶car名字的函數。換句話說,module構造允許你引用你想要的這個綁定。
  • unit表是參數化的帶相對于大多數運作時的值的任意種類的代碼片段。例如,它允許一個代碼片段與一個接受單個參數的car函數一起工作,其中特定函數在稍後通過将片段連接配接到另一個參數被确定。換句話說,unit結構允許你引用滿足某些規範的一個綁定。

除其他外,lambda和class表還允許對稍後選擇的值進行代碼參數化。原則上,其中任何一項都可以以其他任何方式執行。在實踐中,每個表都提供了某些便利——例如允許重寫方法或者特别是對值的特别簡單的應用——使它們适合不同的目的。

從某種意義上說,module表比其它表更為基礎。畢竟,沒有module提供的命名空間管理,程式片段不能可靠地引用lambda、class或unit表。同時,由于名稱空間管理與單獨的擴充和編譯密切相關,module邊界以獨立的編譯邊界結束,在某種程度上阻止了片段之間的互相依賴關系。出于類似的原因,module不将接口與實作分開。

使用unit的情況為,在module本身幾乎可以運作時,但當獨立編譯的部分必須互相引用時,或當你想要在接口(interface)(即,需要在擴充和編譯時間被知道的部分)和實作(implementation)(即,運作時部分)之間有一個更強健的隔離時。更普遍使用unit的情況是,當你需要在函數、資料類型和類上參數化代碼時,以及當參數代碼本身提供定義以和其它參數代碼連結時。

繼續閱讀