FPDIS 2.Lazy Evaluation
Structural Induction on Trees
Martin开篇耿直的说这对于在线课程是可选的(暗示可以跳过)
但是马上又补充了一句说如果你是EPFL的学生,最好别跳,因为考试可能会考
之前的课程中,我们写了一个function set
1 | abstract class Intset { |
如果我们想证明对于某个树T,满足性质P,即P(T)
- 需要先证明对于所有叶子节点l,有P(l)
- 再需要证明对于所有的非叶子节点t, 对于它的所有子树集合s1,…,sn,都满足P(s1) ^ … ^ P(sn)
对于这段代码,需要证明它的正确性
那么,什么是代码的正确性呢
这里指的是代码按照我们预期的方式进行工作,比如,我们可以定义如下规则
- Empty contains x = false
- (s incl x) contains x = true
- (s incl x) contains y = s contians y
对于这段代码,该如何证明呢?
即得易见平凡,仿照上例显然。留作习题答案略,读者自证不难。
反之亦然同理,推论自然成立,略去过程QED,由上可知证毕
Empty contains x = false
这条最简单,看定义易得
1 | object Empty extends IntSet { |
(s incl x) contains x = true
对于Empty的情况
1 | (Empty incl x) contains x |
对于NonEmpty(x, l, r)
1 | (NonEmpty(x, l, r) incl x) contains x |
对于NonEmpty(z, l, r), z < x
1 | (NonEmpty(z, l, r) incl x) contains x |
对于NonEmpty(z, l, r), z > x 同理可得
(s incl x) contains y = s contians y
第三种情况留作习题
Streams, or LazyList ?
从 Scala 2.13 版本开始,Streams
被废弃,推荐使用LazyList
,所以下面用到的例子都是LazyList
1 | val xs = LazyList.cons(1, LazyList.cons(2, LazyList.empty)) |
和List
做个简单对比
1 | def lazyListRange(lo: Int, hi: Int): LazyList[Int] = |
两者的结构基本相同,不同点在于元素是否立刻计算
嗯,还可以用特殊的符号#::
代替LazyList.cons
这里的延迟计算实际上使用传名参数来实现的(call by name)
下面是一个简单的函数签名示例(仅供参考,并不能通过编译)
1 | trait LazyList[+A] extends Seq[A] { |
本节的习题
1 | def lazyListRange(lo: Int, hi: Int): LazyList[Int] = { |
- Nothing
- 1
- 1 2 3
- 1 2 3 4
- 1 2 3 4 5 6 7 8 9
Lazy Evaluation
什么是Lazy
?
Do things as late as possible and never do them twice.
这一点其实受到 Haskell 相当大的影响——这个语言默认就是 Lazy 计算的
为什么 Scala 不这样做呢?
Martin给出的解释:
Scala 允许可变的变量和副作用,这个和Lazy一起搞容易出玄学错误
e.g. 从 StackOverflow 找的一个典型例子
1 | scala> def square(a: =>Int) = a * a |
由于入参 a
是 Call by name 的形式,所以是被延迟计算的,这会导致在a * a
的步骤中被调用了两次println
的副作用——这恰恰是我们不希望发生,却又很容易被忽视的一个问题
所以 Scala 被设计成默认立即计算,但是也可以支持延迟计算
本节习题:
1 | def expr = { |
执行expr
的副作用导致输出结果是什么
- zyxzyx
- xzyz
- xyzz
- zyzz
- something else
Computing with Infinite Sequences
本节简单介绍了下LazyList的应用
- 筛法求素数
- 牛顿开方
习题
1 | val N = 3 |
上面两个表达式哪个生成速度较快?
显然答案是上面那个表达式,因为map
操作不需要额外生成新的元素,
而filter
操作则是每隔N
个元素要判断N-1
次
Case Study: the Water Pouring Problem
本节探讨的问题是倒水问题
倒水问题的简单版本,就是已知有一个水龙头,和一个足够大的水槽,有两个不同容量,没有刻度的水杯。
现在允许做的操作有
- 把水导入水槽
- 从水龙头接水把杯子装满水
- 把一个水杯的水倒入另外一个水杯,知道某个水杯为空或者倒满
现在问,为了量出若干容量的水,所需要的步骤是什么
这个问题讲的很好,Martin一步步抽象问题,解释每一步的意图。
这里就不具体讲代码了,可以直接看课程
编程作业Bloxorz
这个编程作业,是对一个游戏的简化版本自动求解,游戏可以在这里玩到
Game Setup
位置的定义如下
1 | case class Pos(row: Int, col: Int) |
- 行坐标确定垂直方向的位置
- 列坐标确定水平方向的位置
- 坐标原点在左上角,坐标值从上到下,从左到右依次增加
这里用的是坐标系
1 | 0 1 2 3 <- col axis |
我们这样定义地面(Terrain)
1 | type Terrain = Pos => Boolean |
地面可以使用StringParserTerrain.scala
里的方法快速创建
我们需要先实现这个文件里的方法
1 | def terrainFunction(levelVector: Vector[Vector[Char]]): Pos => Boolean = ??? |
具体的要求,在对应的 Scaladoc 里能找到
一个简单的实现如下
1 | def terrainFunction(levelVector: Vector[Vector[Char]]): Pos => Boolean = { |
对场景的函数做好定义后,我们还需要定义好砖块
砖块可以被视为 2*1*1 的立方体
需要用两个字段来描述
1 | case class Block(b1: Pos, b2: Pos) |
砖块可以往平面上的四个方向滚动
需要实现这么几个函数
1 | // 砖块是否处于直立状态 |
下面是一个简单的实现
1 | def isStanding: Boolean = b1 == b2 |
如上所言,我们需要让砖块能在平面上的四个方向滚动,滚动时需要判定新位置是否合法
1 | /** |
简单实现如下
1 | def neighbors: List[(Block, Move)] = { |
Solving the Game
我们把前期的准备工作做完了,接下来就是具体的求解 Solver.scala
实现了
我们可以用LazyList[Block]
来表示具体的解,但是从倒水问题的经验来看,还是很有必要保存之前达到过的状态的,避免重复走到之前走过的位置。
因此,我们用LazyList[(Block, List[Move])]
来表示解答
其中,第二部分List[Move]
表示之前的移动路径
最后一个移动路径,是List[Move]
的首元素 (由于List在头部插入效率远高于尾部插入)
判断是否到达终点的代码很简单
1 | def done(b: Block): Boolean = { |
然后是实现获取下一个状态, 并排除掉已经经过的状态
1 | def neighborsWithHistory(b: Block, history: List[Move]): LazyList[(Block, List[Move])] = { |
Finding Solutions
现在问题的关键在如何求解
现在需要构造from
函数,这个函数会更加初始砖块的位置和已经遍历过的位置,求出所有可能的解空间集合
1 | def from(initial: LazyList[(Block, List[Move])], |
这里可以参照倒水问题的写法
1 | def from(initial: LazyList[(Block, List[Move])], |
详细讲解下这段代码的意图
- 首先处理传参为空的情况
- 对于非空的情况,获取到当前状态的下一个状态
- 过滤掉之前抵达过的砖块状态
把这些结果保存下来,作为下一步可以到达的解空间,同时维护已经抵达过的状态数
剩下的几个函数就是水到渠成的了
1 | lazy val pathsFromStart: LazyList[(Block, List[Move])] = { |
版权声明:
除另有声明外,本博客文章均采用 知识共享(Creative Commons) 署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议 进行许可。
分享