Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

02 Mar 2021

The Racket Guide阅读笔记【四】

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自身也是通过宏定义的。

(本篇完)