Week 6
Racket 基础 Racket Basis
-
定义一个变量
(define x 3)
-
定义一个基于函数调用的变量
(define y (+ x 3))
,这里面+
是一个函数 -
定义一个函数
(define cube1 (lambda (x) (* x (* x x))))
或者(define cube2 (lambda (x) (* x x x)))
-
使用语法糖定义函数
(define (cube3 x) (* x x x))
-
if语句
(if e1 e2 e3)
,e1
为条件语句,e2
为真分支,e3
假分支 -
多参数函数实际上就是柯里化的函数的语法糖,即下二者等价
(define functionName (lambda (x) (lambda (y) (...))))
(define (functionName x y) (...))
-
同样可以定义部分应用
-
但是有一个问题是,如果使用上面的前者,在调用参数时需写成
((functionName x) y)
-
在Racket中,任何括号都是有意义的
- 在大多数情况下,
(e)
是对e
的零参数调用
- 在大多数情况下,
-
函数调用
(cube1 3)
-
racket中的列表操作
- 空列表
null
,在racket中,'()
表示空列表 - 列表创建(元素和列表构建)
cons
- 列表的头元素
car
- 列表的尾部
cdr
- 检查列表是否为空
null?
- 构建一个长列表
(list e1 ... en)
- 空列表
-
racket的语法内容
- 原子项(Atom),
#t, #f, 3, “hi”, null, 5.0, x, ...
- 一些特殊范项(Special Form),原子项的子集
define, lambda, if
- 括号内的一组语法项的序列,
(t1 t2 ... tn)
- 如果第一个语法项是一个特殊范项,那么这个序列是特殊的
- 否则就是一个函数调用
- 任何使用方括号的地方都可以用圆括号代替,但一定要注意对应
- 原子项(Atom),
-
采用括号的形式是很好的,因为使用括号可以将任何程序文本转换成一个树表示,这一过程是平凡且准确的
- 原子项是叶子
- 序列则是以元素为子节点的节点
动态类型指定 Dynamic Typing
- 优——可以不受到类型检查器的限制以构建非常灵活的数据结构
- 劣——很难在真正测试前找到一些小错误,以及在获取内容类型方面比较麻烦
Cond表达式 Cond Expressions
-
避免嵌套的
if
表达式,应使用cond
表达式 -
通用语法
(cond [e1a e1b] [e2a e2b] ... [eNa eNb])
,每一个表达式对中,前一个为条件,后一个为该条件为真时的操作 -
为了实现一个好的格式,最后一个条件需要一定为真
#t
,这样可以保证cond表达式中一定会有一个分支的语句执行,避免所有语句为假时的奇怪情况 -
对于
if
表达式和cond
表达式,条件表达式可以评估为任何事物- 只要不是
#f
的内容都被视为真
- 只要不是
局部绑定 Local Bindings
- 和ML类似,使用
let
指定函数中的局部绑定 - 基本格式:
(let (some local bindings) body)
- 使用中括号分隔开不同的变量:
(let ([x1 e1][x2 e2]...[xn en]) body)
- Racket有四种定义局部绑定的方式
let
let*
letrec
define
- 上述四种定义方式具有不同的语法语义,使用最方便的一种方式(用的最广泛的是
let
) let
可以绑定任意数量个局部变量(注意括号的分布)- 需要注意,
let
表达式中的表达式要在表达式之前的环境中进行评估
- 需要注意,
let*
,其语法规则与上述表达式规则基本无异,唯一区别在于这个是符合ML风格的绑定,即使用当前表达式之前的所有内容进行评估letrec
使用同样的语法规则,rec
是对“recursion”的缩写,其中的表达式用于所有的绑定进行评估,逼近包含之前的所有内容,还包括之后的所有内容(有些类似and用于ML中的相互递归的情形)- 同样可以在局部绑定中用于相互调用的内容
- 但是表达式仍然按照顺序进行评估,如果出现了违例将会出错
- 通常这种“乱序的引用”出现在函数体的定义中,因为在定义时,使用其后内容的函数体将会在let的内容评估后在let的body中被使用并评估,这个时候,函数体后定义的内容已经在函数的闭包中,因此不会出错
- 一个规避性的方法:只在函数绑定中使用晚期绑定(later bindings)
define
同样可以用于函数定义,其语义规则和letrec
相同(在大多数racket用户中受欢迎)
顶级绑定 Top-level Bindings
- 当由大量绑定在文件中时,其工作方式就像
letrec
一样- 类似于ML,可以引用其之前的绑定
- 又不像ML,可以引用其之后的绑定,但只能发生在函数体中(因为其调用即评估的时候是在需要的绑定之后,这是符合顺序的)
- 不像ML,不能在一个模块中定义相同的变量两次
使用set!
进行异变 Mutation with set!
- Racket是具有赋值语句的,但是只在真的合适的地方用
set! x e
- 上述语法中的
x
是已经存在于环境中的变量,e
会评估产生一个结果,并更新绑定x
- 后续对于
x
的查找将会获得新的值,在这之前的查找则是原值(和赋值语句的组用类似)
- Racket中有一个序列操作符
begin
,语法为(begin e1 e2 ... en)
,所有的表达式按顺序执行,其结果为最后一个表达式的值(相当于中间的序列是没有用的)。如果中间的某些表达式有额外影响(使用set!
的异变或者输出),那么这些影响将会被按顺序执行 - 对于顶级绑定,异变存在一些有趣的事实
- 函数的闭包由函数定义是决定,但是函数体只在函数调用时被评估,因此函数中使用绑定将会受到
set!
的影响 - 然而表达式产生了一个值之后,其值将不再与其评估时参与的值相关,因此表达式中的绑定不受到
set!
影响
- 函数的闭包由函数定义是决定,但是函数体只在函数调用时被评估,因此函数中使用绑定将会受到
- 为了避免一些不应当被改变的内容被不慎改变,应当将其在函数内部进行一次本地拷贝
- 一个通用规范:模块外不允许对模块内的内容进行异变
- 一个基本假设:不对顶级绑定进行异变
有关cons
的事实 Truth About Cons
- 可以使用
cons
构造一个对,任何使用cons
构造的结果都是对 - 实际上在很多的动态类型语言中,列表是很多个嵌套的对,其最后一个元素是一个空列表
null
- 对应地,
car
实际上就是访问一个对的第一项,而cdr
则是访问第二项 caddr xs
定义为car(cdr(cdr(xs)))
- 如果末尾是
(cons sth oth)
,那么最终结果将不是一个列表,但仍是一个对 - 相对地,前者被称为恰当列表(proper list),后者称为不恰当列表(improper list)
- 对相对更加有用,而且在一个动态类型语言中,尝试区分一个对的构建和一个列表的拼接并没有什么意义(并不知道后一个元素是一个项目还是一堆项目)
- 不下当列表的项目之间用点隔开
list?
对恰当列表(包括空列表)返回#t
,pair?
对任何使用cons
构造的结果返回#t
用于可异变对的mcons
mcons
for Mutable Pairs
set!
并不会改变cons
中的内容- 为了解决可能需要改变对中的某个元素,提供了
mcons
进行构建 - 其首尾元素使用
mcar
和mcdr
进行访问 - 这样,我们可以对这个构造结果使用
set-mcar!
和set-mcdr!
对特定的对中的元素进行修改 - 使用
mpair?
判断数据结构有效性 - 当我们需要对连接操作的结果的某一项进行异变时,使用
mcons
一般情况下我们则使用cons
延迟评估和形式转换 Delay Evaluation and Thunks
- 不同的语义内容制定了其子表达式在何时被评估
- 函数参数是饥饿式(调用函数前,值就被评估)
- 条件分支不是饥饿式的
- 延迟评估一个表达式:将其放到一个函数里
- 用一个零参数函数实现延迟评估,称之为形式转换(Thunk)
- 对表达式
e
,对其形式转换(lambda() e)
,就能够通过调用这个函数(e)
实现延迟评估(不直接评估,只在调用这个函数时评估内容),其调用结果为其内容结果
避免不必要的计算 Avoiding Unnecessary Computations
- 形式转换让我们能够跳过一些并不需要的昂贵计算
- 假设一些昂贵计算没有额外影响,那么我们要:
- 直到需要时才计算之
- 计算一次就记住这个值,未来使用之则立即完成(懒惰评估)
- Racket已经有了
promises
用于实现懒惰评估,可以使用形式转换以及可异变对实现之 - 形式转换体有一个问题:虽然我们解决了“调用时评估”的问题,但是多次调用时,意味着多次计算
- 一个思路:提前算好这个值一次(传入时评估),存起来,但是仍然产生了问题:如果一直不使用这个值,那么最开始的计算毫无意义
- 延迟函数,用一个对保存计算状态以及需要的计算的形式转换体:
(define (my-delay th) (mcons #f th))
- 强制函数,用于在必要时执行计算,或在已计算完成后直接取值:
(define (my-force p) (if (mcar p) (mcdr p) (begin (set-mcar! p #t) (set-mcdr! p ((mcdr p))) (mcdr p))))
- 另外的一个思路:使用延迟函数和强制函数进行进一步封装(连传入函数时都不进行评估),只在需要时进行调用,并存值(这就是
promises
)
流 Streams
- 一个流是一个无限长的值的序列
- 不可被手动创建
- 一个思路:使用形式转换体以创建序列的绝大部分
- 一种很好的分工
- 流的产生者知道每一个值的产生过程,但不知道要产生多少个
- 流的接收者不知道具体产生过程,但是决定了具体接收多少个值
- 使用对和形式转换体表示流
- 一个流是一个形式转换体,调用之会返回一个对,其头为这个流的第一个元素,尾为表示这个流的第2个到无穷的流的形式转换体,即
‘(next-answer . next-thunk)
- 对流的定义,实际上是一种递归函数的巧妙应用
记忆化 Memoization
- 如果一个函数没有任何额外影响,且不读入任何可异变内容,那么对同一组参数指定两次没有任何意义
- 将之前的计算结果保存在缓存中
- 当下面的条件满足时,这一思路将是对的
- 维护内存比计算要廉价
- 缓存的结果确实被重复用到了
- 和上面的
promises
类似,这里的记忆化需要接受参数,也就意味着有多个可能的结果 - 对于递归函数,使用记忆化方法可以带来指数级的速度提升
宏 Macros
- 一个宏定义描述了如何将一些新语法转换为源语言中的其他语法
- 实际上,宏可以是一种实现语法糖的方式
- 宏系统是一个语言用于定义宏的内容
- 宏扩展是每一次宏使用时重写语法的过程,其发生在程序被运行之前(甚至是编译之前)
- 定义一个宏
m
,那么使用之(m ...)
将会根据定义被扩展 - 宏系统工作在属性字(Token)级别而非字符级别
- 在Racket中使用宏要比其他语言更少担心于括号的使用
- 宏的使用:
(macro-name ...)
- 扩展之后:
(some else ...)
- 宏的使用:
- 对于局部绑定与宏的名称重复,不会出现冲突,Racket会准确地找到需要扩展的内容
- 宏定义的语法:
(define-syntex macro-name (syntex-rules (defined keywords) [(macro-definition one) (expansion one)] [(macro definition two) (expansion two)] ...))
- 延迟宏:宏可以将一个表达式放到形式转换体里面,而不必显式地将其写入形式转换体,这是我们无法用函数实现的(但要注意不是所有的延迟评估都可以用宏实现,因为直接替换内容可能导致错误的多次执行)
- Racket的宏系统可以很“干净”(Hygiene)地解决很多由于单纯替换导致的问题
- 悄悄地将一些宏中的局部变量重命名成一些奇怪的名字以避免冲突
- 在宏定义处查找相关的变量(类似一个词法作用域)