Racket编程指南——6 模块

模块

模块让你把Racket代码组织成多个文件和可重用的库。

    6.1 模块基础

      6.1.1 组织模块

      6.1.2 库集合

      6.1.3 包和集合

      6.1.4 添加集合

    6.2 模块语法

      6.2.1 module表

      6.2.2 #lang简写

      6.2.3 子模块

      6.2.4 main和test子模块

    6.3 模块路径

    6.4 输入:require

    6.5 输出:provide

    6.6 赋值和重定义

 

6.1 模块基础

每个Racket模块通常驻留在自己的文件中。例如,假设文件"cake.rkt"包含以下模块:

"cake.rkt"

#lang racket
(provide print-cake)
; 画一个带n支蜡烛的蛋糕。
(define (print-cake n)
  (show "   ~a   " n #\.)
  (show " .-~a-. " n #\|)
  (show " | ~a | " n #\space)
  (show "---~a---" n #\-))
(define (show fmt n ch)
  (printf fmt (make-string n ch))
  (newline))

然后,其它模块可以导入"cake.rkt"以使用print-cake函数,因为"cake.rkt"中的provide行明确导出这个定义print-cakeshow函数对"cake.rkt"是私有的(即它不能从其它模块被使用),因为show没有被导出。

下面的"random-cake.rkt"模块导入"cake.rkt":

"random-cake.rkt"

#lang racket
(require "cake.rkt")
(print-cake (random 30))

如果"cake.rkt"和"random-cake.rkt"模块在同一个目录里,在导入(require "cake.rkt")中的这个相对引用内的引用"cake.rkt"就会工作。UNIX样式的相对路径用于所有平台上的相对模块引用,就像HTML页面中的相对的URL一样。

6.1.1 组织模块

"cake.rkt"和"random-cake.rkt"示例演示如何组织一个程序模块的最常用的方法:把所有的模块文件在一个目录(也许是子目录),然后有模块通过相对路径相互引用。模块的一个目录可以作为一个项目,因为它可以在文件系统上移动或复制到其它机器上,并且相对路径保存模块之间的连接。

作为另一个例子,如果你正在构建一个糖果分类程序,你可能有一个主"sort.rkt"模块,它使用其它模块访问一个糖果数据库和一个控制分拣机。如果这个糖果数据库模块本身被组织进了处理条码和厂家信息的子模块,那么这个数据库模块可以是"db/lookup.rkt",它使用辅助器模块"db/barcodes.rkt"和"db/makers.rkt"。同样,这个分拣机驱动器"machine/control.rkt"可能会使用辅助器模块"machine/sensors.rkt"和"machine/actuators.rkt"。

"sort.rkt"模块使用相对路径"db/lookup.rkt"和"machine/control.rkt"从数据库和机器控制库导入:

"sort.rkt"

#lang racket
(require "db/lookup.rkt" "machine/control.rkt")
....

"db/lookup.rkt"模块类似地使用相对路径给它自己的源来访问"db/barcodes.rkt"和"db/makers.rkt"模块:

"db/lookup.rkt"

#lang racket
(require "barcode.rkt" "makers.rkt")
....

同上,"machine/control.rkt":

"machine/control.rkt"

#lang racket
(require "sensors.rkt" "actuators.rkt")
....

Racket工具所有工作自动使用相对路径。例如,

  racket sort.rkt

在命令行运行"sort.rkt"程序和自动加载并编译所需的模块。对于一个足够大的程序,从源编译可能需要很长时间,所以使用

  raco make sort.rkt

参见《raco:Racket命令行工具(raco:Racket Command-Line Tools)》中的“raco make: Compiling Source to Bytecode”部分以获取更多关于raco make的信息。

编译"sort.rkt"及其所有依赖成为字节码文件。如果字节码文件存在,运行racket sort.rkt,将自动使用字节码文件。

6.1.2 库集合

一个集合(collection)是一个已安装的库模块的按等级划分的组。一个集合中的一个模块通过一个引号引用,无后缀路径。例如,下面的模块引用"date.rkt"库,它是"racket"集合的一部分:

#lang racket
(require racket/date)
(printf "Today is ~s\n"
        (date->string (seconds->date (current-seconds))))

当你搜索在线Racket文档时,搜索结果显示提供每个绑定的模块。或者,如果你通过单击超链接到达一个绑定文档,则可以在绑定名称上悬停以查找哪些模块提供了它。

一个模块的引用,像racket/date,看起来像一个标识,但它并不是和printfdate->string相同的方式对待。相反,当require发现一个被引号包括的模块引用,它转化这个引用为一个基于集合的模块路径:

  • 首先,如果这个引用路径不包含/,那么require自动添加一个"/main"给这个引用。例如,(require slideshow)等价于(require slideshow/main)

  • 其次,require隐式添加一个".rkt"后缀给这个路径。

  • 最后,require在已安装的集合中通过搜索来决定路径,而不是将路径处理为相对于封闭模块的路径。

作为一个最近似情况,一个集合被实现为一个文件系统目录。例如,"racket"集合大多位于"racket"安装的"collects"目录中的一个"racket"目录中,如以下报告:

#lang racket
(require setup/dirs)
(build-path (find-collects-dir) ; 主集合目录
            "racket")

然而,Racket安装的"collects"目录仅仅是一个require寻找集合目录的地方。其它地方包括用户指定通过(find-user-collects-dir)报告的目录以及通过PLTCOLLECTS搜索路径配置的目录。最后,并且最典型,集合通过安装的包(packages)找到。

6.1.3 包和集合

一个包(package)是通过Racket包管理器(或者作为一个预安装包括在一个Racket分发中)。例如,racket/gui库是由"gui"包提供的,而parser-tools/lex是由"parser-tools"库提供的。

更确切地说,racket/gui由 "gui-lib"提供,parser-tools/lex由"parser-tools-lib"提供,并且"gui"和"parser-tools"包用文档扩展"gui-lib"和"parser-tools-lib"。

Racket程序不直参考包(packages)。相反,程序通过集合(collections)参考库,添加或删除一个改变可获得的基于集合库的集合。一个单个包可以在多个集合中提供库,并且两个不同的包可以在同一集合(但不是同一个库,并且包管理器确保安装的包在该层级不冲突)中提供库。

有关包的更多信息,请参阅《Racket中的包管理》(Package Management in Racket)。

6.1.4 添加集合

回顾《组织模块》部分的糖果排序示例,假设"db/"和"machine/"中的那个模块需要一套公共的助手函数。辅助函数可以被放在一个"utils/"目录里,同时"db/"或"machine/"中的模块可以用开始于"../utils/"的相对路径访问公用模块。只要一组模块在一个单一项目中协同工作,最好保持相对路径。一个程序员可以不用知道你的Racket配置而继承相对路径引用。

有些库是为了被用于跨多个项目,因此将库的源保存在一个目录内与它的使用没有意义。在这种情况下,最好的选择是添加一个新集合。这个库处于一个集合里后,它可以用一个非引用路径引用,就像是包括在Racket发行里的库一样。

你可以通过将文件放置在Racket安装包里或通过(get-collects-search-dirs)报告的一个目录下添加一个新的集合。或者,你可以通过设置PLTCOLLECTS环境变量添加到搜索目录列表。如果你设置PLTCOLLECTS,通过用冒号(UNIX和Mac OS)或分号(Windows)启动这个值包括一个空路径,从而保留原始搜索路径。然而,最好的选择是添加一个

创建一个包并不意味着你必须用一个包服务器或者执行一个复制你的源代码到一个归档格式中的绑定步骤注册。创建一个包只简单地意味着使用包管理器将你的库作为一个来自它们当前源位置的的集合的本地访问。

例如,假设你有一个目录"/usr/molly/bakery",它包含"cake.rkt"模块(来自于本节的开始部分)和其它相关模块。为了使模块可以作为一个"bakery"集合获取,或者

  • 使用raco pkg命令行工具:

      raco pkg install --link /usr/molly/bakery

    当所提供的路径包含一个目录分隔符时,这里--link标记实际上不需要。

  • File(文件)菜单使用DrRacket的Package Manager(包管理器)项。在Do What I Mean(做我打算的)面板,点击Browse...(浏览……),选择"/usr/molly/bakery"目录,并且单击Install(安装)

之后,从任何模块中(require bakery/cake)将从"/usr/molly/bakery/cake.rkt"输入print-cake函数。

默认情况下,你安装的目录的名称既用作名称,又用作包提供的集合。同样,包管理器通常默认只为当前用户安装,而不是在一个Racket安装的所有用户。有关更多信息,请参阅《Racket中的包管理(Package Management in Racket)。

如果你打算分发你的库给其他人,请仔细选择集合和包名称。集合名称空间是分层的,但顶级集合名是全局的,包名称空间是扁平的。考虑将一次性库放在一些顶级名称下,就像"molly"这种标识制造器。在制作烘焙食品库的最终集合时,使用像"bakery"这样的一个集合名。

在你的库被放入一个集合之后,你仍然可以使用raco make以编译库源,但更好而且更方便的是使用raco setup。raco setup命令取得一个集合名(而不是一个文件名)并编译集合内所有的库。此外,raco setup可以建立文档以收集和添加文档到文档索引,作为通过集合中的一个"info.rkt"模块做详细说明。有关raco setup的详细信息请看《raco设置:安装管理(raco setup: Installation Management)》。

 

6.2 模块语法

在一个模块文件的开始的这个#lang开始对一个module表的一个简写,很像'是对一个quote表的一个简写。不同于'#lang简写在REPL内不能正常执行,部分是因为它必须由一个文件结束(end-of-file)终止,也因为#lang的普通写法依赖于封闭文件的名称。

6.2.1 module

既可在REPL又可在一个文件中工作的一个模块声明的普通写法表,是

(module name-id initial-module-path
  decl ...)

其中的name-id是模块的一个名称,initial-module-path是一个初始的输入口,每个decl是一个输入口、输出口、定义或表达式。在一个文件的情况下,name-id通常匹配包含文件的名称,减去其目录路径或文件扩展名,但在模块通过其文件路径requirename-id被忽略。

initial-module-path是必需的,因为为了在模块主体中进一步使用,require表更必须被输入。换句话说,initial-module-path输入引导语法,它在主体内可被使用。最常用的initial-module-pathracket,它提供了本指南中描述的大部分绑定,包括requiredefineprovide。另一种常用的initial-module-pathracket/base,它提供了较少的功能,但仍然是大多数最常用的函数和语法。

例如,前面一节的"cake.rkt"例子可以编写为

(module cake racket
  (provide print-cake)
  (define (print-cake n)
    (show "   ~a   " n #\.)
    (show " .-~a-. " n #\|)
    (show " | ~a | " n #\space)
    (show "---~a---" n #\-))
  (define (show fmt n ch)
    (printf fmt (make-string n ch))
    (newline)))

此外,这个module表可以在一个REPL中被求值以申明一个cake模块,它不与任何文件相关联。为指向是这样一个独立模块,这样引用模块名称:

Examples:
> (require 'cake)
> (print-cake 3)

   ...   

 .-|||-.

 |     |

---------

声明一个模块不会立即求值这个模块的主体定义和表达式。这个模块必须在顶层明确地被require以触发求值。在求值被触发一次之后,后续的require不会重新求值模块主体。

Examples:
> (module hi racket
    (printf "Hello\n"))
> (require 'hi)

Hello

> (require 'hi)

6.2.2 #lang简写

一个#lang简写的主体没有特定的语法,因为这个语法是由接着的#lang语言名称确定。

#lang racket的情况下,语法为:

#lang racket
decl ...

其如同以下内容读取:

(module name racket
  decl ...)

这里name是衍生自包含#lang表的文件的名称。

#lang racket/base表具有和#lang racket同样的语法,除了普通写法的扩展使用racket/base而不是racket#lang scribble/manual表相反,有一个完全不同的语法,甚至看起来不像Racket,在这个指南里我们不准备去描述。

除非另有规定,被作为一个使用#lang记号的“语言”文件化的一个模块将以和#lang racket同样的方式扩展到module。这个文件化的语言名称也可以用modulerequire来直接使用。

6.2.3 子模块

一个module表可以被嵌套在一个模块内,在这种情况下,这个嵌套的module表声明一个子模块(submodule)。子模块可以被使用一个引用名称的外围模块直接引用。下面的例子通过从zoo子模块输入tiger打印"Tony"

"park.rkt"

#lang racket
(module zoo racket
  (provide tiger)
  (define tiger "Tony"))
(require 'zoo)
tiger

运行一个模块不是必须运行其子模块。在上面的例子中,运行"park.rkt"来运行它的子模块zoo仅因为"park.rkt"模块require这个zoo子模块。否则,一个模块及其每一个子模块可以独立运行。此外,如果"park.rkt"被编译成一个字节码文件(通过raco make),那么"park.rkt"代码或zoo代码可以独立加载。

子模块可以嵌套于子模块,而且一个子模块可以被一个模块而不是其外围模块通过使用一个子模块路径(submodule path)直接引用。

一个module*表类似于一个嵌套的module表:

(module* name-id initial-module-path-or-#f
  decl ...)

module*表不同于module在于它反转这个对于子模块和外围模块的参考的可能性:

  • module申明的一个子模块模块可被其外围模块require,但这个子模块不能require这个外围模块或在词法上参考外围模块的绑定。

  • module*申明的一个子模块可以require其外围模块,但是这个外围模块不能require这个子模块。

此外,一个module*表可以在一个initial-module-path的位置指定#f,在这种情况下,子模块领会所有外围模块的绑定——包括没有使用provide输出的绑定。

module*#f申明的子模块的一个使用是通过一个并不从这个模块通常输出的子模块输出附加绑定:

"cake.rkt"

#lang racket
(provide print-cake)
(define (print-cake n)
  (show "   ~a   " n #\.)
  (show " .-~a-. " n #\|)
  (show " | ~a | " n #\space)
  (show "---~a---" n #\-))
(define (show fmt n ch)
  (printf fmt (make-string n ch))
  (newline))
(module* extras #f
  (provide show))

在这个修订的"cake.rkt"模块里,show不是被一个采用(require "cake.rkt")的模块输入,因为大部分"cake.rkt"的客户端不想要这个额外的函数。一个模块可以需要这个使用(require (submod "cake.rkt" extras))访问另外的隐藏show函数的extra子模块

6.2.4 main和test子模块

下面"cake.rkt"的变体包括一个调用print-cakemain子模块:

"cake.rkt"

#lang racket
(define (print-cake n)
  (show "   ~a   " n #\.)
  (show " .-~a-. " n #\|)
  (show " | ~a | " n #\space)
  (show "---~a---" n #\-))
(define (show fmt n ch)
  (printf fmt (make-string n ch))
  (newline))
(module* main #f
  (print-cake 10))

运行一个模块不会运行其module*定义的子模块。尽管如此,还是可以通过racket或DrRacket运行上面的模块打印一个带10支蜡烛的蛋糕,因为main子模块是一个特殊情况。

当一个模块作为一个程序名称提供给racket可执行文件或在DrRacket中直接运行,如果这个模块有一个main子模块,这个main子模块会在其外围模块之后运行。当一个模块直接运行时,声明一个main子模块从而指定额外的行为去被执行,以代替require作为在一个更大的程序里的一个库。

一个main子模块不必用module*声明。如果main模块不需要使用其外围模块的绑定,则可以被用module来声明。更通常的是,main使用module+来声明:

(module+ name-id
  decl ...)

module+申明的一个子模块就像一个以使用#f作为其initial-module-pathmodule*申明的子模块。此外,多个module+表可以指定相同的子模块名称,在这种情况下,module+表的主体被组合以创建一个单独的子模块。

module+的组合行为对定义一个test子模块是非常有用的,它可以使用raco test方便地运行,用同样的方式main也可以方便地使用racket运行。例如,下面的"physics.rkt"模块输出dropto-energy函数,并且它定义了一个test模块以支持单元测试:

"physics.rkt"

#lang racket
(module+ test
  (require rackunit)
  (define ε 1e-10))
(provide drop
         to-energy)
(define (drop t)
  (* 1/2 9.8 t t))
(module+ test
  (check-= (drop 0) 0 ε)
  (check-= (drop 10) 490 ε))
(define (to-energy m)
  (* m (expt 299792458.0 2)))
(module+ test
  (check-= (to-energy 0) 0 ε)
  (check-= (to-energy 1) 9e+16 1e+15))

输入"physics.rkt"到一个更大的程序不会运行dropto-energy测试——即使引发这个测试代码的加载,如果模块被编译——但在一个命令行中运行raco test physics.rkt会运行这个测试。

上边的"physics.rkt"模块相当于使用module*

"physics.rkt"

#lang racket
(provide drop
         to-energy)
(define (drop t)
  (* 1/2 49/5 t t))
(define (to-energy m)
  (* m (expt 299792458 2)))
(module* test #f
  (require rackunit)
  (define ε 1e-10)
  (check-= (drop 0) 0 ε)
  (check-= (drop 10) 490 ε)
  (check-= (to-energy 0) 0 ε)
  (check-= (to-energy 1) 9e+16 1e+15))

使用module+代替module*允许对交错函数定义进行测试。

module+的组合行为有时也是对一个main模块有帮助的。即使在组合不被需要时,(module+ main ....)因为它比(module* main #f ....)更具可读性而是首选。

 

6.3 模块路径

一个模块路径(module path)是对一个模块的一个引用,作为require的使用,或者作为一个module表中的initial-module-path。它可以是几种表中的任意一种:

(quote id)

一个引用标识的一个模块路径(module path)指的是使用这个标识的一个非文件module声明。模块引用的这种表在一个REPL中具有更多意义。

Examples:
> (module m racket
    (provide color)
    (define color "blue"))
> (module n racket
    (require 'm)
    (printf "my favorite color is ~a\n" color))
> (require 'n)

my favorite color is blue

rel-string

一个字符串模块路径是使用UNIX样式约定的一个相对路径:/是路径分隔符,..指父目录,.指同一目录。rel-string不必以一个路径分隔符开始或结束。如果路径没有后缀,".rkt"会自动添加。

这个路径是相对于封闭文件,如果有的话,或者是相对于当前目录。(更确切地说,路径是相对于 (current-load-relative-directory)的值),这是在加载一个文件时设置的。

模块基础》使用相对路径显示示例。

如果一个相对路径以一个".ss"后缀结尾,它会被转换成".rkt"。如果实现引用模块的文件实际上以".ss"结束,当试图加载这个文件(但一个".rkt"后缀优先)时后缀将被改回来。这种双向转换提供了与Racket旧版本的兼容。

id

作为一个引用标识的一个模块路径引用一个已经安装的库。这个id约束只包含ASCII字母、ASCII数字、+-_/,这里/分隔标识内的路径元素。这个元素指的是集合(collection)子集合(sub-collection),而不是目录和子目录。

这种表的一个例子是racket/date。它是指模块的源是"racket"集合中的"date.rkt"文件,它被安装为Racket的一部分。".rkt"后缀被自动添加。

这种表的另一个例子是racket,它通常被使用在初始输入时。这个路径racket是对racket/main的简写;当一个id没有/,那么/main自动被添加到结尾。因此,racketracket/main指的是其源是"racket"集合里的"main.rkt"文件的模块。

Examples:
> (module m racket
    (require racket/date)
  
    (printf "Today is ~s\n"
            (date->string (seconds->date (current-seconds)))))
> (require 'm)

Today is "Monday, November 14th, 2022"

当一个模块的完整路径以".rkt"结束时,如果没有这样的文件存在但有一个".ss"后缀的文件存在,那么这个".ss"后缀自动被替代。这种转换提供了与Racket旧版本的兼容。

(lib rel-string)

像一个非引号标识路径,但表示为一个字符串而不是一个标识。另外,rel-string可以以一个文件后缀结束,在这种情况下,".rkt"不被自动添加。

这种表的例子包括(lib "racket/date.rkt")(lib "racket/date"),这等效于racket/date。其它的例子包括(lib "racket")(lib "racket/main")(lib "racket/main.rkt"),都等效于racket

Examples:
> (module m (lib "racket")
    (require (lib "racket/date.rkt"))
  
    (printf "Today is ~s\n"
            (date->string (seconds->date (current-seconds)))))
> (require 'm)

Today is "Monday, November 14th, 2022"

(planet id)

通过PLaneT服务器访问一个被分发的第三方库。这个库在其被需要的第一时间被下载,然后使用这个本地副本。

这个id编码了被一个/分隔的几条信息:包所有者,然后是带可选版本信息的包名称,以及对一个带包的特定库的一个可选路径。就像id作为一个 lib路径、一个被自动添加的".rkt"后缀以及在没有子路径提供时被用作路径的/main的简写。

Examples:
> (module m (lib "racket")
    ; 使用"schematics""random.plt" 1.0, 文件"random.rkt":
    (require (planet schematics/random:1/random))
    (display (random-gaussian)))
> (require 'm)

0.9050686838895684

如同其它表,如果没有以".rkt"结尾的执行文件存在,一个以".ss"结尾的实现文件可以自动被取代。

(planet package-string)

就像一个planet的符号表,但使用一个字符串而不是一个标识。同样,package-string可以以一个文件后缀结束,在这种情况下,".rkt"不被添加。

与其它表一样,在如果没有以".rkt"结束的的执行文件存在而一个以".ss"结束的实现文件可以自动被替代时,一个".ss"扩展名转换为".rkt"。

(planet rel-string (user-string pkg-string vers ...))
vers=nat
|(nat nat)
|(= nat)
|(+ nat)
|(- nat)

一般更通用的表去访问来自于PLaneT服务器的一个库。在这种通用表中,一个PLaneT引用像一个有一个相对路径的lib引用那样开始,但这个路径后面跟随关于库的制造者、包和版本的信息。指定的包被按需下载和安装。

这个vers在这个包的可接受版本上指定一个约束,这里一个版本号是一个非负整数序列,并且这个约束确定序列中的每个元素的允许值。如果没有为一个特定元素提供约束,则任何版本被允许;特别是,省略所有vers意味着任何版本都被接受。强烈推荐至少指定一个vers

对于一个版本约束,一个单纯的nat(+ nat)相同,其匹配nat或高于这个版本号的相应元素。一个(start-nat end-nat)匹配包括在start-natend-nat范围内的任何数值。一个(= nat)恰好匹配nat。一个(- nat)匹配nat或更低的。

Examples:
> (module m (lib "racket")
    (require (planet "random.rkt" ("schematics" "random.plt" 1 0)))
    (display (random-gaussian)))
> (require 'm)

0.9050686838895684

自动的".ss"和".rkt"转换应用为其它表。

(file string)

指定一个文件,其string是一个使用当前平台的约定的相对或绝对路径。这个表不是轻量的,并且当一个单纯的、轻量的rel-string足够时,它应该不(not)被使用。

这个自动的".ss"和".rkt"转换应用为其它表。

(submod base element ...+)
base=module-path
|"."
|".."
element=id
|".."

是指base的一个子模块。在submod中的element的序列指定一个子模块名称的路径以到达最终的子模块。

Examples:
> (module zoo racket
    (module monkey-house racket
      (provide monkey)
      (define monkey "Curious George")))
> (require (submod 'zoo monkey-house))
> monkey

"Curious George"

使用"."作为在submod中的base代表外围模块。使用".."作为base等效于使用"."后跟一个额外的".."。当一个表(quote id)的路径指一个子模块时,它等效于(submod "." id)

使用".."作为一个element取消一个子模块步骤,实际上指定外围模块。例如,(submod "..")指路径出现在其中的子模块的封闭模块。

Examples:
> (module zoo racket
    (module monkey-house racket
      (provide monkey)
      (define monkey "Curious George"))
    (module crocodile-house racket
      (require (submod ".." monkey-house))
      (provide dinner)
      (define dinner monkey)))
> (require (submod 'zoo crocodile-house))
> dinner

"Curious George"

 

6.4 输入:require

require表从其它模块输入。一个require表可以出现在一个模块中,在这种情况它将来自于指定模块的绑定引入到输入模块中。一个require表也可以出现在顶层,在这种情况它既输入绑定也实例化(instantiates)指定的模块;更确切地说,如果主体定义和指定模块的表达式还没有被求值则对其求值。

一个单个的require可以同时指定多个输入:

(require require-spec ...)

在一个单一的require表里指定多个require-spec与使用多个每个包含一个单一require-spec的require本质上是一样的。其区别很小,且局限于顶层:一个单一的require可以最多一次导入一个给定标识,而一个单独的require可以代替一个以前的require(都是仅在顶层,在一个模块之外)的绑定。

一个require-spec的允许形态被递归地定义为:

module-path

在其最简单的表中,一个require-spec是一个module-path(如前一节《模块路径》中定义的)。在这种情况下,被require引入的这个绑定通过provide声明来确定,申明中的每个模块被各个module-path引用。

Examples:

> (module m racket
    (provide color)
    (define color "blue"))
> (module n racket
    (provide size)
    (define size 17))
> (require 'm 'n)
> (list color size)

'("blue" 17)

(only-in require-spec id-maybe-renamed ...)
id-maybe-renamed=id
|[orig-id bind-id]

一个only-in表限制被一个低级的require-spec引入的绑定的设置。此外,only-in选择性地重命名每个可保存的绑定:在一个[orig-id bind-id]表里,orig-id引用一个被require-spec隐含的绑定,并且bind-id是这个在输入上下文中将被绑定的名称,以代替orig-id。

Examples:

> (module m (lib "racket")
    (provide tastes-great?
             less-filling?)
    (define tastes-great? #t)
    (define less-filling? #t))
> (require (only-in 'm tastes-great?))
> tastes-great?

#t

> less-filling?

less-filling?: undefined;

 cannot reference an identifier before its definition

  in module: top-level

> (require (only-in 'm [less-filling? lite?]))
> lite?

#t

(except-in require-spec id ...)

这个表是 only-in的补充:它从被require-spec指定的集合中排除指定的绑定。

(rename-in require-spec [orig-id bind-id] ...)

这个表支持像only-in的重命名,但自不作为一个orig-id提交的require-spec中分离单独的标识。

(prefix-in prefix-id require-spec)

这是一个重命名的简写,这里prefix-id被添加到被require-spec指定的每个标识的前面。

除了only-in、except-in、rename-in2和prefix-in表可以被嵌套以实现输入绑定的更复杂的操作。例如,

(require (prefix-in m: (except-in 'm ghost)))

输入m输出的所有绑定,除了ghost绑定之外,并带用m:前缀的局部名称:

等价地,这个prefix-in可以被应用在except-in之前,只是带except-in的省略是被使用m:前缀所指定:

(require (except-in (prefix-in m: 'm) m:ghost))

 

6.5 输出:provide

默认情况下,一个模块的所有定义对这个模块是私有的。provide表指定定义以造成在模块被require的地方可获取。

(provide provide-spec ...)

一个provide表只能出现在模块级(即在一个module的当前主体中)。在一个单一的provide中指定多个provide-spec和使用每个带有一个单一的provide-spec的多个provide明显是一样的。

每个标识遍及这个模块中的所有provide最多可以从一个模块中输出一次。更确切地说,用于每个输出的外部名称必须是不同的;相同的内部绑定可以用不同的外部名称输出多次。

一个provide-spec的允许形态被递归定义为:

identifier

在最简单表中,一个provide-spec标明一个在被输出的模块内的绑定。这个绑定既可以来自于一个局部定义,也可以来自于一个输入。

(rename-out [orig-id export-id] ...)

一个rename-out表类似于只指定一个标识,但这个输出绑定orig-id是给定一个不同的名称,export-id,到输入模块。

(struct-out struct-id)

一个struct-out表输出被(struct struct-id ....)创建的绑定。

参见《程序员定义的数据类型》以获取define-struct的更多信息。

(all-defined-out)

all-defined-out简写输出所有在输出模块中(与输入相反)被定义的绑定。

all-defined-out简写的使用通常被阻止,因为它导致对一个模块的实际导出不太明确,并且因为Racket程序员习惯于认为定义可以自由地添加到一个模块而不影响其公共接口(在all-defined-out被使用时候不是这样)。

(all-from-out module-path)

all-from-out简写输出在使用一个基于module-path的require-spec输入的模块中的所有绑定。

尽管不同的module-path可以适用于同一个基于文件的模块,但带all-from-out的重复输出是明确基于module-path引用,而不是被实际引用的模块。

(except-out provide-spec id ...)

就像provide-spec,但省略每个id的输出,其中id是绑定到省略的外部名称。

(prefix-out prefix-id provide-spec)

就像provide-spec,但为每个输出的绑定添加prefix-id到外部名称的开头。

 

6.6 赋值和重定义

在一个模块中的变量定义上的set!使用仅限于定义模块的主体。也就是说,一个模块被允许去改变它自己定义的值,这样的变化对于输入模块是可见的。但是,一个输入上下文不允许去更改一个被输入的绑定的值。

Examples:

> (module m racket
    (provide counter increment!)
    (define counter 0)
    (define (increment!)
      (set! counter (add1 counter))))
> (require 'm)
> counter

0

> (increment!)
> counter

1

> (set! counter -1)

set!: cannot mutate module-required identifier

  at: counter

  in: (set! counter -1)

在上述例子中,一个模块通过提供一个突变函数总是能够授予其它能力以改变其输出,如increment!。

在输入变量工作上的禁令有助于支持程序的模块化推理。例如,在这个模块中,

(module m racket
  (provide rx:fish fishy-string?)
  (define rx:fish #rx"fish")
  (define (fishy-string? s)
    (regexp-match? rx:fish s)))

函数fishy-string?将始终匹配包含“fish”的字符串,不管其它模块如何使用rx:fish绑定。出于同样的帮助程序员的原因,在对输入工作上的禁令也允许许多程序去被更有效地执行。

在同一行中,当一个模块不包含一个在这个模块中定义的特定标识的set!,那么该标识被认为一个不会被改变的常量(constant)——即使通过重新声明该模块。

因此,一个模块的重定义通常不被允许。对于基于文件的模块,简单地更改该文件不会导致任何情况下的一个重新声明,因为基于文件的模块是按需加载的,而先前加载的声明满足将来的请求。它有可能使用Racket的反射支持以重新声明一个模块,然而,非文件模块可以在REPL中被重新声明;在这种情况下,如果重新申明涉及一个以前的静态绑定的重定义,也许会失败。

> (module m racket
    (define pie 3.141597))
> (require 'm)
> (module m racket
    (define pie 3))

define-values: assignment disallowed;

 cannot re-define a constant

  constant: pie

  in module:'m

作为探索和调试目的,Racket反射层提供一个compile-enforce-module-constants参数来使常量的执行无效。

> (compile-enforce-module-constants #f)
> (module m2 racket
    (provide pie)
    (define pie 3.141597))
> (require 'm2)
> (module m2 racket
    (provide pie)
    (define pie 3))
> (compile-enforce-module-constants #t)
> pie

3

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值