泛函编程(34)-泛函变量:处理状态转变-ST Monad
泛函编程的核心模式就是函数组合(compositionality)。实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code)。所谓纯代码就是程序中的所有表达式都必须是Referentially Transparent(RT,等量可替换的),它的意思是:在一段程序p中,所有的表达式e都可以用e的运算结果替代而不影响到p的运算结果,那么e就是RT等量可替换的,也就是说程序p是由纯代码组成的。但如果程序p中包含了一些变量,这些变量的状态就会影响到程序中e的运算结果,那么p就不再是纯代码了,也就无法保证函数组合的正确性了。所以在泛函编程模式中好像是禁止任何状态变化的(state mutation)。但实际上泛函编程并没有任何明文禁止一个函数内部使用状态转变,所以:如果一个函数f(x)的输入参数x是RT等量可替换的,那么函数f还是个纯函数(pure function)。
为了方便或者提高运算效率,我们往往可能在一个函数内部使用一些变量(local variables)。如果这些变量的状态转变只体现在函数内部,那么对于这个函数的用户来说,这是个纯函数,使用这个函数进行函数组合是没有问题的。我们看看下面的这个例子:
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(l: Int, r: Int, pivot: Int) = {
val pivotVal = arr(pivot)
swap(pivot, r)
var j = l
for (i <- l until r) if (arr(i) < pivotVal) {
swap(i, j)
j += 1
}
swap(j, r)
j
}
def qs(l: Int, r: Int): Unit = if (l < r) {
val pi = partition(l, r, l + (r - l) / 2)
qs(l, pi - 1)
qs(pi + 1, r)
}
qs(0, arr.length - 1)
arr.toList
}
}
以上函数即使使用了while loop, 变量var及可变数组Array,但这些都被限制在函数内部,所以quicksort还是个纯函数。
但是,使用了局部变量后往往迫使代码变得很臃肿。程序变得复杂影响了代码的理解、维护及重复利用。
泛函编程采用的是一种处理变量状态变化的编程语言。在前面我们已经讨论过State Monad,它可以对状态进行读写。State Monad的运作模式是:S => (A,S),即:传入一个状态S,产生一个新的值及新的状态。对于处理本地状态转变,我们不是要对传入的S进行处理,而是把它作为一种标记让拥有同样标示S的函数可以对变量进行转变。
针对以上需求,一个新的数据类型产生了:ST Monad,我们看看它的定义:
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 (a1,s1) = self.run(s)
(f(a1),s1)
}
}
def flatMap[B](f: A => ST[S,B]): ST[S,B] = new ST[S,B] {
def run(s: S) = {
val (a1,s1) = self.run(s)
f(a1).run(s1)
}
}
}
object ST {
def apply[S,A](a: A): ST[S,A] = {
lazy val memo = a
new ST[S,A] {
def run(s: S) = (memo, s)
}
}
}
这个ST和State基本上一致,只是状态转变函数run不对外开放:protected def run(s: S): (A,S),这是由于S代表了可以转变状态的权限,我们希望把这个权利局限在ST类内部。ST实现了flatMap,所以是个Monad。
我们希望达到的目的是通过内存参考(memory reference)对变量状态转变进行控制。我们需要实现的方法包括:
分配新的内存单元(memory cell)
读取内存单元数据
存写内存单元数据
ST是个Monad,我们可以制造一个for-comprehension的Monadic语言来进行泛函变量状态转变。我们的变量类型数据结构封装了一个变量:protected var,如下:
trait STRef[S,A] {
protected var cell: A
def read: ST[S,A] = ST(cell)
def write(a: A): ST[S,Unit] = new ST[S,Unit] {
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
})
}
可以看到,STRef的读写访问都返回ST。这使得我们可以用ST Monad语言来描述变量状态转变,如下:
for {
r1 <- STRef[Nothing,Int](1)
r2 <- STRef[Nothing,Int](2)
x <- r1.read
y <- r2.read
_ <- r1.write(y + 1)
_ <- r2.write(x + 1)
a <- r1.read
b <- r2.read
} yield (a,b)
下一步就是如何运算以上的表达式了。我们希望能安全的运算变量状态转变,那么考虑以下两种ST操作:
ST[S,STRef[S,A]
ST[S,Int]
前面的ST动作包括了一个变量参考,使用者能通过STRef来修改变量,这个操作是不安全的。
ST[S,Int]包含了一个值,所以这个ST动作是安全的。
我们希望借scala的类系统(type system)来帮助我们阻止不安全的ST操作成功编译(compile)。具体实现方式如下:
trait RunnableST[A] {
def apply[S]: ST[S,A]
}
我们先增加一个新的类型RunnableST。把类参数S嵌入在RunnableST类内部的apply方法里。这样可以有效防止new RunnableST[STRef[Nothing,Int]]这样的语句通过编译。再增加一个可以运算ST的函数runST:
object ST {
def apply[S,A](a: A): ST[S,A] = {
lazy val memo = a
new ST[S,A] {
def run(s: S) = (memo, s)
}
}
def runST[S,A](rst: RunnableST[A]) =
rst[Null].run(null)._1
}
现在我们可以运算变量状态变化描述的程序了:
val prg = 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)
} //> prg : ch14.ex2.RunnableST[(Int, Int)] = ch14.ex2$$anonfun$main$1$$anon$6@6
//| 108b2d7
ST.runST(prg) //> res1: (Int, Int) = (3,2)
我们知道,Array类型也是一种内存参考。我们也可以建一个基于Array的泛函变量数据类型:
class STArray[S,A] (implicit manifest: Manifest[A]) {
protected val value: Array[A]
//array 长度
def size: ST[S,Int] = ST(value.size)
//读取array i 位置
def read(i: Int): ST[S,A] = ST(value(i))
//将a写入array i 位置
def write(i: Int, a: A): ST[S,Unit] = new ST[S,Unit] {
def run(s: S) = {
value(i) = a
((),s)
}
}
//将可变array转换成不可变list
def freeze: ST[S,List[A]] = ST(value.toList)
//按照Map的指引,把Map.v写入array Map.k位置
def fill(xs: Map[Int,A]): ST[S,Unit] =
xs.foldRight(ST[S,Unit](())) {
case ((k,v), st) => st flatMap {_ => write(k,v)}
}
//array位置i,j内容互换
def swap(i: Int, j: Int): ST[S,Unit] = for {
x <- read(i)
y <- read(j)
_ <- write(i, y)
_ <- write(j, x)
} yield () }
object STArray {
//建一个长度为sz,初始值为v的array
def apply[S,A: Manifest](sz: Int, v: A) = ST(new STArray[S,A] {
lazy val value = Array.fill(sz)(v)
})
//把一个List转成STArray
def fromList[S,A: Manifest](xs: List[A]): ST[S, STArray[S,A]] = ST(new STArray[S,A] {
lazy val value = xs.toArray
})
}
再看看用STArray的例子:
object Immutable {
def noop[S] = ST[S,Unit](()) def partition[S](a: STArray[S,Int], l: Int, r: Int, pivot: Int): ST[S,Int] = for {
vp <- a.read(pivot)
_ <- a.swap(pivot, r)
j <- STRef(l)
_ <- (l until r).foldLeft(noop[S])((s, i) => for {
_ <- s
vi <- a.read(i)
_ <- if (vi < vp) (for {
vj <- j.read
_ <- a.swap(i, vj)
_ <- j.write(vj + 1)
} yield ()) else noop[S]
} yield ())
x <- j.read
_ <- a.swap(x, r)
} yield x def qs[S](a: STArray[S,Int], l: Int, r: Int): ST[S, Unit] = if (l < r) for {
pi <- partition(a, l, r, l + (r - l) / 2)
_ <- qs(a, l, pi - 1)
_ <- qs(a, pi + 1, r)
} yield () else noop[S] def quicksort(xs: List[Int]): List[Int] =
if (xs.isEmpty) xs else ST.runST(new RunnableST[List[Int]] {
def apply[S] = for {
arr <- STArray.fromList(xs)
size <- arr.size
_ <- qs(arr, 0, size - 1)
sorted <- arr.freeze
} yield sorted
})
}
从以上的讨论我们了解到:泛函变量状态变化是先用Monadic语言描述状态转变然后通过类系统来实现安全运算的。
泛函编程(34)-泛函变量:处理状态转变-ST Monad的更多相关文章
- 泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(17)-泛函状态-State In Action
对OOP编程人员来说,泛函状态State是一种全新的数据类型.我们在上节做了些介绍,在这节我们讨论一下State类型的应用:用一个具体的例子来示范如何使用State类型.以下是这个例子的具体描述: 模 ...
- 泛函编程(30)-泛函IO:Free Monad-Monad生产线
在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...
- 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...
- 泛函编程(38)-泛函Stream IO:IO Process in action
在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...
- 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink
上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...
- 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process
IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...
随机推荐
- Node.js入门:异步IO
异步IO 在操作系统中,程序运行的空间分为内核空间和用户空间.我们常常提起的异步I/O,其实质是用户空间中的程序不用依赖内核空间中的I/O操作实际完成,即可进行后续任务. 同步IO的并行模式 ...
- Java线程:线程的交互
一.线程交互的基础知识 SCJP所要求的线程交互知识点需要从java.lang.Object的类的三个方法来学习: void notify() 唤醒在此对象监视器上等 ...
- Atitit 图像处理30大经典算法attilax总结
Atitit 图像处理30大经典算法attilax总结 1. 识别模糊图片算法2 2. 相似度识别算法(ahash,phash,dhash)2 3. 分辨率太小图片2 4. 横条薯条广告2 5. 图像 ...
- 每天一个linux命令(34):du 命令
Linux du命令也是查看使用空间的,但是与df命令不同的是Linux du命令是对文件和目录磁盘使用的空间的查看,还是和df命令有一些区别的. 1.命令格式: du [选项][文件] 2.命令功能 ...
- 学习Data Science/Deep Learning的一些材料
原文发布于我的微信公众号: GeekArtT. 从CFA到如今的Data Science/Deep Learning的学习已经有一年的时间了.期间经历了自我的兴趣.擅长事务的探索和试验,有放弃了的项目 ...
- Ucos系统常用的数据结构有哪些?
1)表 链表 表中主要了解链表,尤其是单向链表. 2)数组 一维数组 二维数组 使用数组有什么好处,在c语言中,数组是一组连续数字的集合它们数组的下标,代表了数组的相对位置,所以说,在一些高效的查表过 ...
- Redis安装配置与Jedis访问数据库
一.NOSQL概要 NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库.NoSQL数据库的四大分类 键值(Key-Value)存储数据库 这一类数据 ...
- javascripts学习笔记(五):用js来实现缩略语列表、文献来源链接和快捷键列表。
1 缩略语列表问题出发点:一段包含大量缩略语的文本,例如: <p> The <abbr title="World Wide Web Consortium"> ...
- Windows Azure Web Site (14) Azure Web Site IP白名单
<Windows Azure Platform 系列文章目录> 我们知道,在Azure Cloud Service和Virtual Machine,可以通过Endpoint ACL (Ac ...
- JAVA 设计模式 组合模式
用途 组合模式 (Component) 将对象组合成树形结构以表示“部分-整体”的层次结构.组合模式使得用户对单个对象和组合对象的使用具有唯一性. 组合模式是一种结构型模式. 结构