1. 纯函数式的可变状态
1.1 引用透明和纯粹的定义
- 引用透明:倘若表达式e是引用透明的,那么对于所有的程序p而言,其中的e都可以用其求值后的结果替换,且不影响p的原始含义
- 纯粹:倘若一个函数f是纯粹的,那么表达式f(x)对于所有引用透明的x而言也是引用透明的
1.2 引入可变类型的纯粹函数
- arr为可变数组
- 所有对可变数组的引用都在quicksort内部
- quicksort外部没有任何代码对引用可变数组
- 因此整个quicksort是纯粹的、引用透明的
def quicksort(xs: List[Int]):List[Int] = if(xs.isEmpty) xs else {
val arr = xs.toArray
def swap(x: Int, y: Int) = { // 交换数组中的两个元素
val tmp = arr(x)
arr(x) = arr(y)
arr(y) = tmp
}
def partition(n: Int, r: Int, pivot: Int) = { //将数组的元素与pivot比较,比他大的和比他小的各分一部分
val pivotVal = arr(pivot)
swap(pivot, r)
var j = n
for(i <- n until r) if (arr(i) < pivotVal){
swap(i, j)
j += 1
}
swap(j, r)
j
}
def qs(n: Int, r: Int): Unit = if(n < r){ // 基于原数组的分治排序
val pi = partition(n, r, n + (r - n) / 2)
qs(n, pi - 1)
qs(pi + 1 , r)
}
qs(0, arr.length - 1)
arr.toList
}
2. 限制可变性的语言表达
S => (A, S)输入一个状态,而后产生一个结果和下一个状态
sealed trait ST[S, A]{self =>
protected def run(s: S): (A, S)
def map[B](f: A => B): ST[S, B] = new ST[S, B]{
def run (s: S) = {
val (a, s1) = self.run(s)
(f(a), s1)
}
}
def flatMap[B](f: A => ST[S, B]): ST[S,B] = new ST[S, B]{
def run(s: S) = {
val (a, s1) = self.run(s)
f(a).run(s1)
}
}
}
object ST{
def apply[S,A](a: => A) = {
lazy val memo = a
new ST[S,A]{
def run(s: S) = (memo, s)
}
}
}
3. 限制可变性的代数表达(使用实例)
应用ST构建计算和其他各种操作,完成增、改、查三个操作
sealed trait STRef[S, A]{
protected var cell: A
def read: ST[S, A] = ST(cell) // 读操作:通过ST.apply创建了一个新的ST对象,纯粹方法,因为返回了ST
def write(a: A): ST[S, Unit] = new ST[S, Unit]{ // 写操作:通过new一个ST操作,并完善了其中的run方法(protected,实例化时不需要再加protect修饰词),纯粹方法,因为返回了ST
def run(s: S) = {
cell = a
((), s)
}
}
}
object STRef{
def apply[S,A](a: A):ST[S, STRef[S, A]] = ST(new STRef[S, A]{var cell = a}) // 使用了ST.apply方法,实例化STRef对象时指定了其中的cell变量
}
// 测试一下
for {
r1 <- STRef[Nothing, Int](1)
r2 <- STRef[Nothing, Int](1)
x <- r1.read
y <- r2.read
_ <- r1.write(y+1)
_ <- r2.write(x+1)
a <- r1.read
b <- r2.read
}yield(a,b)
4. 修改状态
- ST monad的意义:应用ST构建计算,执行过程是:分配内部的可变状态 -> 处理可变状态完成计算任务 -> 丢弃可变状态。整个计算过程都是引用透明的,因为所有可变状态都是被局限在内部不可见。
- 怎么安全的运行ST的行为
不安全:ST[S, STRef[S, Int]]返回了一个可变引用,STRef会涉及到类型S,当S发生改变,会导致STRef发生变化,STRef中的S涉及到外部变量,不再引用透明,如果存在函数中存在STRef,则函数不是纯粹的
安全:ST[S, Int]返回了一个不可变引用,ST[S, Int]其实就是一个Int,即使其中涉及到局部的可变状态,但是不关心S到底是什么,对S而言,生成Int的一系列行为只是一种多态的表现- 安全执行ST的行为,体现行为基于S的多态性
trait RunnableST[A]{
def apply[S]: ST[S, A]
}
val p = new RunnableST[(Int, Int)]{
def apply[S] = for{
r1 <- STRef(1)
r2 <- STRef(2)
x <- r1.read
y <- r2.read
_ <- r1.write(y+1)
_ <- r2.write(x+1)
a <- r1.read
b <- r2.read
}yield(a, b)
}
// 实现runST函数调用任务RunnableST的apply,无所谓S的选择。由于RunnableST行为的多态性,调用过程中保证不会用到,所以传一个()是绝对安全的,也就是Unit类型
// 函数runST函数的定义必须在ST的伴生对象上,因为run在特质ST中是受保护的(protected修饰符),除了伴生对象以外,其他对方都无法访问
object ST{
def apply[S,A](a: => A) = {
lazy val memo = a
new ST[S,A]{
def run(s: S) = (memo, s)
}
}
def runST[A](st: RunnableST[A]): A =
st.apply[Unit].run(())._1
}
// 测试一下
val p = new RunnableST[(Int, Int)]{
def apply[S] = for{
r1 <- STRef(1)
r2 <- STRef(2)
x <- r1.read
y <- r2.read
_ <- r1.write(y+1)
_ <- r2.write(x+1)
a <- r1.read
b <- r2.read
}yield(a, b)
}
val r = ST.runST(p)
5. 可变数组
5.1 一个数组的ST monad
sealed abstract class STArray[S, A](implicit manifest: Manifest[A]){ // 传入默认值来构建数组
protected def value: Array[A]
def size: ST[S, Int] = ST(value.size)
def write(i: Int, a: A): ST[S, Unit] = new ST[S, Unit]{ // 根据索引将值写入数组
def run(s: S) = {
value(i) = a
((), s)
}
}
def read(i: Int):ST[S, A] = ST(value(i)) // 根据索引从数组读取值
def freeze: ST[S, List[A]] = ST(value.toList) // 将该数组转换成不可变list
}
object STArray{
def apply[S, A: Manifest](sz: Int, v: A): ST[S, STArray[S, A]] = // 构造一个给定长度的数组,并用v填满他
new STArray[S, A]{
lazy val value = Array.fill(sz)(v)
}
}
5.2 一个纯函数的in-place快排实现
def swap[S](i: Int, j: Int):ST[S, Unit] = for{
x <- read(i)
j <- read(j)
_ <- write(i, y)
_ <- write(j, x)
}yield()
def partition[S](arr: STArray[S, Int], n: Int, r: Int, pivot: Int): ST[S, Int]
def qs[S](a: STArray[S, Int], n: Int, r: Int): ST[S, Unit]
// 测试一下
def quicksort(xs: List[Int]):List[Int] =
if(xs.isEmpty) xs else ST.runST(new RunnableST[List[Int]]{
def apply[S] = for{
arr <- STArry.fromList(xs)
size <- arr.size
_ <- qs(arr, 0, size - 1)
sorted <- arr.freeze
}yield sorted
})
6. 基于上下文的视角理解纯函数
- 引用透明不代表引用的对象一成不变
Foo(“hello”) eq Foo(“hello”)返回False,每次都是创建一个全新的对象,引用的对象发生了改变- 引用透明:如果在程序p中每个出现的表达式e都被其求值的结果替换,对程序p不造成任何影响,我们可以说在当前上下文p中e是引用透明的
7.基于上下文的视角理解副作用
- 副作用例子
def timesTwo(x: Int) = {
if(x<0) println("Got a negative number")
x*2
}
// 如果用2替换timesTwo(1),程序将改变,程序的副作用(控制台输出)将被忽略,因此timesTwo引用不透明
- 副作用取决于我们是否关注上下文的改变:副作用是否值得追踪,我们是否关注相应副作用,如标准输出