Racket Modules

模块基础

一个模块可以被另一个模块使用,但是模块内部的变量不能直接修改,即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)