模块基础
一个模块可以被另一个模块使用,但是模块内部的变量不能直接修改,即set!
通常,每个Racket模块驻留在自己的文件中,换句话说,可以认为一个.rkt 文件里的内容属于某一个模块,模块名就是文件名。
在任意目录下创建一个子目录:mod_demo
├── mod_demo
│ ├── ext
│ │ └── cake.rkt
│ └── main.rkt
mod_demo目录下有一个main.rkt模块,和一个ext目录,该目录下有一个cake.rkt的模块。
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))
(provide print-cake) 意味着将函数print-cake导出,在模块外部就可以使用该函数了;还有一个私有的函数show, 该函数未导出,所以外部无法访问。 main.rkt的内容为:
#lang racket
(require "ext/cake.rkt")
(print-cake (random 30))
(require “ext/cake.rkt”) 将cake模块引入,引用路径符合Unix风格(但是,不能以文件分隔符即’/‘作为开始或者结束),也支持相对路径,(require “./ext/cake.rkt”) 也是完全可以的。
Collections
collection中的模块通过不带引号的、无后缀的路径引用。例如:以下模块引用了作为“racket” collection一部分的“date.rkt”库:
#lang racket
(require racket/date)
(printf "Today is ~s\n"
(date->string (seconds->date (current-seconds))))
注意,(require racket/date)没有带双引号。像racket/date这样的模块看起来像一个标识符,当require看到一个未加引号的模块引用时,它会将引用转换为基于collection的模块路径:
- 首先,如果未引用的路径不包含 /,然后 require会自动向引用添加一个“/main”。例如,( require slideshow )等效于( require slideshow/main )。
- 其次,require 向路径隐式添加了“.rkt”后缀。
- 最后,require通过在已安装的collection中搜索来解析路径,而不是将路径视为相对于封闭模块的路径。
Packages and Collections
一个package是一组通过Racket包管理器安装的库的集合。Racket 程序不直接引用包。相反,程序通过collections引用库,并且添加或删除包会更改可用的基于集合的库集。
添加Collections
library的旨在跨多个项目使用,因此将库的源文件保存在一个目录中并没有意义,更不能将库复制到不同的项目中使用。在这种情况下,最好的选择是新增一个collection,将lib放在collection中,这样就可以使用不带引号的路径引用它,就像Racket 发行版中包含的库一样。
不用担心,创建一个包无需提交到公开的包服务器,可以安装到本地来使用。
采用raco pkg命令行工具: raco pkg install –link /path/to/mod_demo
安装之后,在任何模块中使用(require mod_demo/ext/cake)都会从/path/to/mod_demo/ext/cake.rkt导入print-cake含函数。
> (require mod_demo/ext/cake)
> (print-cake 4)
......
.-||||||-.
| |
------------
>
默认情况下,您安装的目录名称既用作包名称又用作包提供的collection
将lib放入collection之后,仍然可以使用raco make来编译库的源文件,但是使用raco setup更加方便。尤其是修改了模块代码后,使用raco setup会重新编译所有库文件,并重新安装包。与raco make不同的是,raco setup 后面的参数是包名,即raco setup mod_demo, 而raco make后面是模块名,即raco make main.rkt。
模块语法
模块文件开头处的#lang其实是模块形式的简写,但不能用于REPL。
模块形式
普通形式的模块声明,可以工作于REPL
(module name-id initial-module-path decl …)
name-id是模块的名字,initial-module-path 为初始化导入,每个decl可以是导入,或者导出,或者定义,或者表达式。
initial-module-path是必须的,模块内部也是一个环境(SICP中的环境,即上下文),内部使用的指令是需要通过initial-module-path引导的。常用的initial-module-path是racket, require/define/provide等等都来自racket。另一个常用的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)))
这种模块形式是可以被REPL求值的(注意(require ‘cake), 模块名cake需要被quote,因为这时cake是非文件的模块声明):
> (require 'cake)
> (print-cake 3)
...
.-|||-.
| |
---------
声明一个模块,其body不会被立刻求值,只有在显式地被require之后才会求值一次。
> (module hi racket
(printf "Hello\n"))
> (require 'hi)
Hello
> (require 'hi)
#lang
#lang声明的模块的body没有特定的语法,因为其语法由#lang之后的名称所决定。
比如,#lang racket的语法是:
#lang racket
decl ...
等同于:
(module name racket
decl ...)
name是包含#lang形式的文件名
子模块
一个模块可以嵌套在另一个模块中,父模块可以直接访问子模块导出的函数、定义、表达式。
#lang racket
(module zoo racket
(provide tiger)
(define tiger "Tony"))
(require 'zoo)
tiger
module*
module*形式类似module:
(module* name-id initial-module-path-or-#f
decl ...)
module*与module的不同之处在于:
- 由module声明的子模块,可以被其父模块require, 但是子模块不能require父模块;
- 由module*声明的字模块,可以require父模块,但是父模块不能require该子模块;
此外,module*形式可以用#f代替initial-module-path,这意味着,子模块可以访问父模块所以绑定,包括未通过provide导出的绑定。
因此,用module*和#f来声明一个模块的一个用途是,将某个模块未provide出去的绑定导出。
#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))
尽管show函数未导出,但是子模块extras却将其导出了,外部程序可以使用(require (submod “caske.rkt” extras))来访问隐藏的show函数。
Main和Test子模块
#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))
这个“cake.rkt”变体,包含了一个main子模块,并调用了print-cake函数。
一般来说,运行一个模块时并不会运行其中由module*声明的子模块,但是main子模块除外。
main子模块并不一定由module*声明,如果不需要使用父模块的绑定,也可以由module来声明。 更常见的做法是由module+来声明
``lisp (module+ name-id decl …)
module+声明的模块就像采用module*声明且使用#f作为initial-module-path的模块。此外,多个module+模块可以同名,同名的模块会组合成一个模块。这种组合特性可以用来定义一个test模块,在使用 raco test命令时就可以大显身手了。
假设"physics.rkt" 为:
```lisp
#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" 时,并不会运行drop和to-energy的测试,不过运行raco test physics.rkt将会执行这些测试。
这等价于使用module*:
#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+的组合行为对main模块也有帮助,即使不需要组合, ( module+ main …. )也是首选,因为它比( module* main #f …. )更具可读性 。
Require
require的定义为:
(require require-spec …)
only-in
用来限制模块导出的绑定,也可重命名绑定
> (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
是only-in的补充,用来排除某些绑定
(except-in require-spec id …)
rename-in
与only-in类似。
(rename-in require-spec [orig-id bind-id] …)
prefix-in
(prefix-in prefix-id require-spec) 给每一个require-spec的绑定添加前缀
Provide
(provide provide-spec …)
provide-spec允许递归定义:
id: 最简单的形式,上文中多次出现
(rename-out [orig-id export-id] …): 重命名导出的绑定
(struct-out struct-id): 导出struct
(all-defined-out): 导出所有绑定,不推荐
(all-from-out module-path): 导出所有由module-path指定的绑定
(except-out provide-spec id …): 排除id指定的绑定
(prefix-out prefix-id provide-spec):给每个导出绑定添加一个前缀
欢迎加入Racket 隐修会 :731859928(QQ)