天天看点

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的情况是,当你需要在函数、数据类型和类上参数化代码时,以及当参数代码本身提供定义以和其它参数代码链接时。

继续阅读