【scala函数式编程】可变状态

1. 纯函数式的可变状态

1.1 引用透明和纯粹的定义

  1. 引用透明:倘若表达式e是引用透明的,那么对于所有的程序p而言,其中的e都可以用其求值后的结果替换,且不影响p的原始含义
  2. 纯粹:倘若一个函数f是纯粹的,那么表达式f(x)对于所有引用透明的x而言也是引用透明的

1.2 引入可变类型的纯粹函数

  1. arr为可变数组
  2. 所有对可变数组的引用都在quicksort内部
  3. quicksort外部没有任何代码对引用可变数组
  4. 因此整个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. 修改状态

  1. ST monad的意义:应用ST构建计算,执行过程是:分配内部的可变状态 -> 处理可变状态完成计算任务 -> 丢弃可变状态。整个计算过程都是引用透明的,因为所有可变状态都是被局限在内部不可见。
  2. 怎么安全的运行ST的行为
    不安全:ST[S, STRef[S, Int]]返回了一个可变引用,STRef会涉及到类型S,当S发生改变,会导致STRef发生变化,STRef中的S涉及到外部变量,不再引用透明,如果存在函数中存在STRef,则函数不是纯粹的
    安全:ST[S, Int]返回了一个不可变引用,ST[S, Int]其实就是一个Int,即使其中涉及到局部的可变状态,但是不关心S到底是什么,对S而言,生成Int的一系列行为只是一种多态的表现
  3. 安全执行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. 基于上下文的视角理解纯函数

  1. 引用透明不代表引用的对象一成不变
    Foo(“hello”) eq Foo(“hello”)返回False,每次都是创建一个全新的对象,引用的对象发生了改变
  2. 引用透明:如果在程序p中每个出现的表达式e都被其求值的结果替换,对程序p不造成任何影响,我们可以说在当前上下文p中e是引用透明的

7.基于上下文的视角理解副作用

  1. 副作用例子
    def timesTwo(x: Int) = {
        if(x<0) println("Got a negative number")
        x*2
    }
// 如果用2替换timesTwo(1),程序将改变,程序的副作用(控制台输出)将被忽略,因此timesTwo引用不透明
  1. 副作用取决于我们是否关注上下文的改变:副作用是否值得追踪,我们是否关注相应副作用,如标准输出
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鱼摆摆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值