社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
模块让你把Racket代码组织成多个文件和可重用的库。
每个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-cake。show函数对"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一样。
"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,将自动使用字节码文件。
一个集合(collection)是一个已安装的库模块的按等级划分的组。一个集合中的一个模块通过一个引号引用,无后缀路径。例如,下面的模块引用"date.rkt"库,它是"racket"集合的一部分:
#lang racket (require racket/date) (printf "Today is ~sn" (date->string (seconds->date (current-seconds))))
当你搜索在线Racket文档时,搜索结果显示提供每个绑定的模块。或者,如果你通过单击超链接到达一个绑定文档,则可以在绑定名称上悬停以查找哪些模块提供了它。
一个模块的引用,像racket/date,看起来像一个标识符,但它并不是和printf或date->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)找到。
一个包(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)。
回顾《组织模块》部分的糖果排序示例,假设"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)》。
在一个模块文件的开始的这个#lang开始对一个module表的一个简写,很像'是对一个quote表的一个简写。不同于',#lang简写在REPL内不能正常执行,部分是因为它必须由一个文件结束(end-of-file)终止,也因为#lang的普通写法依赖于封闭文件的名称。
既可在REPL又可在一个文件中工作的一个模块声明的普通写法表,是
(module name-id initial-module-path decl ...)
其中的name-id是模块的一个名称,initial-module-path是一个初始的输入口,每个decl是一个输入口、输出口、定义或表达式。在一个文件的情况下,name-id通常匹配包含文件的名称,减去其目录路径或文件扩展名,但在模块通过其文件路径require时name-id被忽略。
initial-module-path是必需的,因为为了在模块主体中进一步使用,require表更必须被输入。换句话说,initial-module-path输入引导语法,它在主体内可被使用。最常用的initial-module-path是racket,它提供了本指南中描述的大部分绑定,包括require、define和provide。另一种常用的initial-module-path是racket/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 "Hellon")) > (require 'hi) Hello
> (require 'hi)
一个#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记号的“语言”文件化的一个模块将以和#langracket同样的方式扩展到module。这个文件化的语言名称也可以用module或require来直接使用。
一个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子模块。
下面"cake.rkt"的变体包括一个调用print-cake的main子模块:
"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+来声明: