The Racket Guide阅读笔记,chapter 5, 6.
5 Programmer-Defined Datatypes
5.1 Simple Structure Types: struct
struct可以用来创建自定义的类型。语法如下:
(struct struct-id (field-id ...))
例子:
(struct posn (x y))
默认struct不限制字段的类型。如果需要限制,则要使用Contracts。
5.2 Copying and Update
(struct-copy struct-id struct-expr [field-id expr] ...)
可以用来克隆一个struct。克隆之后的struct和原来的struct具有相同的字段,但不相等。可以在克隆的时候顺便跟新字段。
> (define p1 (posn 1 2))
> (define p2 (struct-copy posn p1 [x 3]))
5.3 Structure Subtypes
可以用struct struct-id super-id (field-id ...))
来定义struct的子类型。
(struct posn (x y))
(struct 3d-posn posn (z))
子类型不会有父类型的字段选择器。上面的3d-posn的定义不会生成3d-posn-x。
5.4 Opaque versus Transparent Structure Types
默认情况下struct打印的时候不显示字段,通过加上#:transparent
关键字可以让打印的时候带上字段内容。
(struct posn (x y)
#:transparent)
> (posn 1 2)
(posn 1 2)
通透的struct支持反射操作,比如struct?
以及struct-info
等等。
5.5 Structure Comparisons
equal?
默认会递归对比struct的每一个字段。但是equal?
只会对比实例的统一性:
> (struct lead (width height))
> (define slab (lead 1 2 ))
> (equal? slab (lead 1 2))
#f
可以把struct对比的任务交给某个方法。为此,需要使用关键字#:menthods来指定目标方法gen:equal+hash,内含三个方法:
- equal-proc
- hash-proc
- hash2-proc
5.6 Structure Type Generativity
多次调用struct会产生不同的struct,即便它们的字段相同。
5.7 Prefab Structure Types
使用#:prefab可以让struct的定义与prefab (“previously fabricated”)的结构兼容:
> (define lunch '#s(sprout bean))
> (struct sprout (kind) #:prefab)
> (sprout? lunch)
#t
一个prefab结构体也可以有另一个prefac结构体作为上形:
> (struct building (rooms [location #:mutable]) #:prefab)
> (struct house building ([occupied #:auto]) #:prefab
#:auto-value 'no)
> (house 5 'factory)
'#s((house (1 no) building 2 #(1)) 5 factory no)
prefab可以只有常量值没有结构体。
现在结构体的形式可以分为多种:
- Opagque (默认):必须通过struct的定义来生成。没有定义无法得知其内容
- Transparent:必须通过struct的定义来生成。但是实例的内容自描述。
- Prefab:不需要通过struct的定义生生成。
5.8 More Structure Type Options
struct-options:
- #:mutable,将字段设置为可改,并会为struct的字段生成set修改器
- #:mutable也可以指定在field级别
- #:transparent
- #:inspector,泛化#:transparent,用以支持更多访问控制选项
- #:prefab,
- #:auto-value,用于给:auto字段指定默认值
- #:guard,指定构造函数
- #:methods,用于实现某个接口的方法
- #:property,用于实现property
- #:super,用于指定上级
6 Modules
6.1 Module Basics
一个racket模块大概存放在一个文件中。
一个模块cake.rkt具有以下内容:
#lang racket
(provide print-cake)
...
那么另一个模块random-cake.rkt可以导入cake.rkt:
#lang racket
(require "cake.rkt")
(print-cake (random 30))
cake.rkt和random-cake.rkt必须在同一个目录。
6.1.1 Organizing Modules
如何用子目录来组织模块。
6.1.2 Library Collections
安装好的模块成为合集(collection),可以有层次结构。
一个例子:
(require racket/date)
其查找逻辑如下:
- 如果这个无引用的合集没有包含
/
,那么require会自动为其加上/main
。也就是(require slideshow)
等同于(require slideshow/main)
- 然后require会加上".rkt"后缀名
- 最后require会在合集中寻找这个文件
6.1.3 Packages and Collections
合集对应操作系统的一个文件,可以通过下面的代码打印其目录:
> (require setup/dirs)
> (build-path (find-collects-dir) "racket")
一个包是一系列库的集合,可以通过包管理器安装,也可以通过racket安装包发行。例如racket/gui
库由gui包提供,parser-tools/lex
由parser-tools提供。而gui本身是对于gui-lib的扩展;parser-tools本身是对parser-tools-lib的扩展。
6.1.4 Adding Collections
合集的目录可以通过(get-collects-search-dirs).
打印,也可以通过PLTCOLLECTS环境变量修改。可以直接添加一个合集,但是最好还是通过包来管理。
假设目录"/usr/molly/bakery"下有"cake.rkt"模块,可以通过下面的命令安装一个名叫bakery的合集(同时这个包也叫做bakery):
raco pkg install --link /usr/molly/bakery
DrRacket中可以通过File菜单来安装。
之后就可以通过(require bakery/cake)
的方式来引用"cake.rkt"中的定义了。
如果要发行你的模块,选择合集和封包名字的时候要小心。合集名字可以是层次的,但是首层名字是全局的。封包名字却是扁平的。
把封库放入合集之后依然可以使用raco make来构建。但是更好的方法是通过raco setup来构建。后者还会从"info.rkt"中生成文档。
6.2 Module Syntax
源文件开头的#lang
隐含声明了一个模块。但是在REPL里面#lang
不好使。
6.2.1 The module Form
可以使用下列方式声明模块:
(module name-id initial-module-path
decl ...)
name-id通常对应文件名,initial-module-path通常是racket或者racket/base。
一个例子:
(module cake racket
(provide print-cake)
---)
使用的时候可以(require 'cake)
。
模块中的代码在首次require的时候计算。
6.2.2 The #lang Shorthand
#lang racket
对应着(module name racket ---)
,此处的name来自于文件名。
6.2.3 Submodules
module定义可以嵌套在另一个模块内部。请求一个模块并不会自动执行其子模块的代码。
还可以使用module*来定义子模块,和module定义的子模块不同:
- module*定义的子模块只能从子模块引用父模块。
- module*可以在initial-module-path处使用#f。
6.2.4 Main and Test Submodules
module*定义的内容不会在父模块被请求的时候调用。但是主模块除外。 主模块中定义的子模块会在父模块代码执行完成后执行。
定义一个主模块:
(module* main #f
(print-cake 10))
主模块中的子模块可以看作是经由module+定义的。多个module+定义的子模块可以同名,它们的内容会被整合成一体。
在定义测试模块的时候module+特别有用。(module+ test ---)
的内容可以在raco test
的时候自动执行。
使用(module+ main ....)
可能更方便。
6.3 Module Paths
模块路径是到模块的一个引用。模块路径使用裹标识符表示。
相对模块路径采用unix的相对文件路径表示。用(current-load-relative-directory)
可以打印相对路径的锚。如果相对路径以".ss"结尾,则会被自动转化为".rkt"。
模块路径采用解标识符,则模块是来自于安装的程式库。此处的解标识符能包含的字符包括ASCI字母数字以及+ - _ /
。此时模块路径指向合集或者子合集。
一个例子是racket/date
,其指向racket
合集中的date.rkt
。
不以'/‘结尾的解标识符会被自动加上/main
,比如racket
会转为racket/main
。
模块路径采用解标识符,跟(lib rel-string)
同等效果。另外lib形式可以指定.rkt后缀。
(planet id)
指向第三方的托管在PLaneT
服务器上的库。类似的还有(planet package-string)
以及(planet rel-string (user-string pkg-string vers ...))
。
(file string)
指向一个文件。
可以用submod指向一个子模块:
> (module zoo racket
(module monkey-house racket
(provide monkey)
(define monkey "Curious George")))
> (require (submod 'zoo monkey-house))
> monkey
"Curious George"
6.4 Imports: require
require之前见过多次了。出现在顶层的require不仅导入一个模块,还会执行相应的代码。
require可以导入多个模块。一次性导入多个模块的时候,后面模块的绑定可能会覆盖前面模块的绑定。
可以用only-in限制导入的绑定个数,例如:
(require (only-in 'm tastes-great?))
可以用except-in来排除不想导入的绑定。
rename-in和only-in有点类似,但是未列出的绑定也会被导入。
prefix-in在导入的绑定的名字前面加上前缀。
上述提到的这几个-in可以互相嵌套发挥作用。
6.5 Exports: provide
默认情况下,所有模块的声明只有自身可见,除非通过provide提供给外部。provide形式只可以出现在模块级别。
(provide provide-spec ...)
中的provide-spec
可以是下列形式:
- 标识符
(rename-out [orig-id export-id] ...)
(struct-out struct-id)
导出(struct struct-id ....)
创建的绑定(all-defined-out)
(all-from-out module-path)
导出从module-path导入的绑定(except-out provide-spec id ...)
,导出的时候忽略某些绑定(prefix-out prefix-id provide-spec)
,导出的时候加上某些前缀
6.6 Assignment and Redefinition
set!
只可以修改本模块内的变量。导入其他模块是,只能同构导入的函数来修改其他模块内的变量。
基于文件的模块,当文件重新命名以后可能不会被自动加载。非文件模块可以在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
如果处于调试目的,可以通过compile-enforce-module-constants
关掉上述约束。
6.7 Modules and Macros
racket的模块系统和宏系统相互合作,给racket带来强大的语法定义能力。
导入racket/base之后会展露require以及lambda语法,导入其他模块的时候会展露更多语法。
define-syntax-rule自身也是通过宏定义的。
(本篇完)