泛函编程(17)-泛函状态-State In Action
对OOP编程人员来说,泛函状态State是一种全新的数据类型。我们在上节做了些介绍,在这节我们讨论一下State类型的应用:用一个具体的例子来示范如何使用State类型。以下是这个例子的具体描述:
模拟一个自动糖果贩售机逻辑:贩售机有两种操作方法:投入硬币和扭动出糖旋钮。贩售机可以处于锁定和放开两种状态。模拟运作跟踪贩售机内当前的糖果和硬币数量。贩售机的操作逻辑要求如下:
1、如果机内有糖的话,投入硬币贩售机从锁定状态进入放开状态
2、在放开状态下扭动旋钮贩售机放出一块糖果后自动进入锁定状态
3、在锁定状态下扭动旋钮贩售机不做反应
4、在放开状态下投入硬币贩售机不做反应
5、没有糖果的贩售机对任何操作都不做反应
我们先把涉及到的数据类型设计出来:
type candy = Int //方便表达
type coin = Int //方便表达
sealed trait Input
case object Coin extends Input //投币
case object Turn extends Input //旋钮
case class Machine(locked: Boolean, candies: candy, coins: coin) //状态类
Machine类型就是需要变迁的状态,状态内容包括锁定状态locked,当前糖果数candies,当前硬币数coins。
我们的模拟函数款式如下:
def simulateMachine(inputs: List[Input]): State[Machine,coin]
输入一个操作动作List,返回State实例,值是当前硬币数,状态是Machine,里面包括了完成操作后的锁定状态、硬币数、糖果数。
我们先做一个比较直接容易明白的版本。首先是贩售机互动逻辑部分:这部分模拟了操作互动流程并在运行过程中对状态进行变迁,最终以输出形式返回最新状态:
def transition(input: Input, machine: Machine): Machine = {
(input, machine) match {
case (_, Machine(_,0,_)) => machine
case (Turn, Machine(true,_,_)) => machine
case (Coin, Machine(false,_,_)) => machine
case (Coin, Machine(true, _, nCoin)) => machine.copy(locked = false, coins = nCoin + 1)
case (Turn, Machine(false, nCandy, _)) => machine.copy(locked = true, candies = nCandy - 1)
}
}
这个transition函数采用了泛函状态维护风格:传入一个状态;返回新状态。流程逻辑部分是通过分析操作动作及贩售机当前状态来决定如何更新状态;一切按照设计方案要求进行。状态machine是个case class实例,这样我们可以使用machine.copy来复制一个新的状态,内容包括locked,candies,coins、又或者在某些情况下,原封不动地返回传入的状态。很明显,transition就是我们需要的状态行为函数,只要嵌入一个State实例就可以随时实现状态变迁了。
transition只能处理单个操作动作,那么如果我们输入一个List的操作动作该如何连续处理呢?既然涉及List,自然想到用递归算法应该能行:
def execute(inputs: List[Input], machine: Machine): Machine = {
inputs match {
case Nil => machine
case h::t => execute(t, transition(h,machine)) }
}
在execute函数里我们对List里每个操作元素进行transition运算,状态machine也在一连串的transition运算中自动更新了。有了这两个函数我们就很容易推断出整体流程了:获取初始状态 >>> 以此初始状态输入操作处理流程并把最终结果设定为当前状态 >>> 读取当前状态:
for {
s0 <- getState //读取起始状态
_ <- setState(execute(inputs,s0)) //人工设定新状态
s1 <- getState //读取当前状态
} yield s1.coins
整个大流程还是比较容易理解的。我们注意到状态变迁采用了临时手工设定方式 setState。整个程序代码和运行结果示范如下:
type candy = Int //方便表达
type coin = Int //方便表达
sealed trait Input
case object Coin extends Input
case object Turn extends Input
case class Machine(locked: Boolean, candies: candy, coins: coin) def simulateMachine(inputs: List[Input]): State[Machine,coin] = {
def transition(input: Input, machine: Machine): Machine = {
(input, machine) match {
case (_, Machine(_,0,_)) => machine
case (Turn, Machine(true,_,_)) => machine
case (Coin, Machine(false,_,_)) => machine
case (Coin, Machine(true, _, nCoin)) => machine.copy(locked = false, coins = nCoin + 1)
case (Turn, Machine(false, nCandy, _)) => machine.copy(locked = true, candies = nCandy - 1)
}
}
def execute(inputs: List[Input], machine: Machine): Machine = {
inputs match {
case Nil => machine
case h::t => execute(t, transition(h,machine)) }
}
for {
s0 <- getState //读取起始状态
_ <- setState(execute(inputs,s0)) //人工设定新状态
s1 <- getState //读取当前状态
} yield s1.coins
} //> simulateMachine: (inputs: List[ch6.state.Input])ch6.state.State[ch6.state.M
//| achine,ch6.state.coin] val inputs = List(Coin, Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn)
//> inputs : List[Product with Serializable with ch6.state.Input] = List(Coin,
//| Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn)
simulateMachine(inputs).run(Machine(true,3,0)) //> res0: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3))
以上这段代码考虑到了OOP编程人员的思维模式,采用了分段表达方式使整个程序变得更容易理解。对比起来,下面的例子就可以说是真正的泛函编程风格了。同样针对以上的贩售机模拟逻辑要求,我们将用典型的泛函风格来编程。我们先看看下面两个函数:
def modify[S](f: S => S): State[S,Unit] = {
for {
s0 <- getState
_ <- setState(f(s0))
} yield ()
}
def sequence[S,A](xs: List[State[S,A]]): State[S,List[A]] = {
def go(s: S, actList: List[State[S,A]], acc: List[A]): (List[A],S) = {
actList match {
case Nil => (acc.reverse, s) //纠正排序
case h::t => h.run(s) match {case (a2,s2) => go(s2,t,a2 :: acc) }
}
}
State(s => go(s,xs,List()))
}
modify比较直白:取出当前状态,对这个状态进行变迁后再设成当前状态。sequence稍微复杂一点。我们先从它的类型匹配开始分析:接收一个List[State]、输出State[List],换句话说就是把一连串的状态变成一个状态内的一连串值。这不刚好和我们模拟函数要求匹配吗?我们要求一个函数对一连串的操作动作进行处理后产生一个最终的状态。sequence函数内部已经包含了处理循环,我们不需要execute函数了。但是这个版本的sequence函数比较低级:我是指它使用了递归算法,必须在函数内部实现状态行为run(s)。如果我们用高价一点的函数实现sequence,有可能不需要理会run(s)了:
//用右折叠:输入与输出同排序,但不是tail recursive
def sequenceByRight[S,A](xs: List[State[S,A]]): State[S,List[A]] = {
xs.foldRight(unit[S,List[A]](List())){ (f,acc) => f.map2(acc)(_ :: _) }
}
//用左折叠:输入与输出反排序,是tail recursive
def sequenceByLeft[S,A](l: List[State[S, A]]): State[S, List[A]] = {
l.reverse.foldLeft(unit[S, List[A]](List()))((acc, f) => f.map2(acc)( _ :: _ ))
}
无论用左右折叠算法都可以实现sequence功能。注意:我们没有使用run(s),因为这个东西是在flatMap里,而map2是用flatMap实现的。用这种高阶函数使程序更加简洁。
def simulateMachineFP(inputs: List[Input]): State[Machine,coin] = {
for {
_ <- sequence{
inputs.map {
input => modify {
machine: Machine => {
(input, machine) match {
case (_, Machine(_,0,_)) => machine
case (Turn, Machine(true,_,_)) => machine
case (Coin, Machine(false,_,_)) => machine
case (Coin, Machine(true, nCandy, nCoin)) => Machine(false,nCandy, nCoin+1)
case (Turn, Machine(false, nCandy, nCoin)) => Machine(true, nCandy - 1, nCoin)
}
}
}
}
}
s <- getState
} yield s.coins
} //> simulateMachineFP: (inputs: List[ch6.state.Input])ch6.state.State[ch6.state
//| .Machine,ch6.state.coin]
simulateMachineFP(inputs).run(Machine(true,3,0)) //> res1: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3))
哇,有点过了!不过这里的确没有分段编码,一口气用sequence完成了编程。那我们还是来分析一下:sequence需要接收一个参数类型是:List[State[S,A]] 我们有一个List[Input],需要把这个List[Input] 变成 List[State[S,A]],很明显,我们需要用map来做这个转换 List[Input].map{ Input => State[S,A]}。modify返回类型State[S,Unit],所以我们用了input => modify,这在类型上是匹配的。modify,顾名思义就是更新状态,我们把状态变迁逻辑都放到了modify函数里,它的返回结果就是最终的状态。
_ <- sequence ...modify 起到了 _ <- setState 的作用,所以我们可以用 s <- getState 把最新的状态读出来。
在以上这个例子里我们采用了泛函编程风格:用类型匹配方式进行了函数组合,虽然说代码可能简单了,但清洁可能就说不上了。需要用类型匹配(type line-up)来分析理解,也就是要再熟悉多点泛函编程思考模式。
后面补充一下:如果我来选择,我会稍退一步;把逻辑部分提出来:
def simulateMachineConcise(inputs: List[Input]): State[Machine,coin] = {
def transition(input: Input, machine: Machine): Machine = {
(input, machine) match {
case (_, Machine(_,0,_)) => machine
case (Turn, Machine(true,_,_)) => machine
case (Coin, Machine(false,_,_)) => machine
case (Coin, Machine(true, nCandy, nCoin)) => Machine(false,nCandy, nCoin+1)
case (Turn, Machine(false, nCandy, nCoin)) => Machine(true, nCandy - 1, nCoin)
}
} for {
_ <- sequence{inputs.map {input => modify {machine: Machine => transition(input, machine)}}} s <- getState
} yield s.coins
} //> simulateMachineConcise: (inputs: List[ch6.state.Input])ch6.state.State[ch6.
//| state.Machine,ch6.state.coin]
simulateMachineConcise(inputs).run(Machine(true,3,0))
//> res2: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3))
这段核心代码是不是简洁多了,也比较容易理解:
for {
_ <- sequence{inputs.map {input => modify {machine: Machine => transition(input, machine)}}} s <- getState
} yield s.coins
泛函编程(17)-泛函状态-State In Action的更多相关文章
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(38)-泛函Stream IO:IO Process in action
在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...
- 泛函编程(34)-泛函变量:处理状态转变-ST Monad
泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Re ...
- 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process
IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...
- 泛函编程(29)-泛函实用结构:Trampoline-不再怕StackOverflow
泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法.比如说flatMap就是一种推进式的递归算法,没了它就无法使用for-comprehension,那么泛函编程也就无 ...
- 泛函编程(25)-泛函数据类型-Monad-Applicative
上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...
- 泛函编程(24)-泛函数据类型-Monad, monadic programming
在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...
- 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink
上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...
- 泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...
随机推荐
- 安装vs2013以后,链接数据库总是报内存损坏,无法写入的错误
安装vs2013以后,链接数据库总是报内存损坏,无法写入的错误 这个错误几个月以前解决过一次,但是到又碰到的时候,竟然完全忘记当时怎么解决的了, 看来上了年纪记忆真是越来越不行了... 解决方案很简单 ...
- 开发Android必知的工具
程序开发有时候非常依赖使用的开发工具,好的完备的开发工具可以让开发人员的工作效率有大幅度的提高.开发Android也是如此,大家可能都离不开Eclipse或Android Studio这些工具,但他们 ...
- javascript方法 call()和apply()的用法
先上代码: apply()方法示例 /*定义一个人类*/ function Person(name,age) { this.name=name; this.age=age; } /*定义一个学生类*/ ...
- kail2在虚拟机上的安装
首先先要安装虚拟机,打开安装包,下一步 选择典型 选择要安装到的目录,点下一步 4 输入密钥,下一步(密钥网上有很多我这边就例举一个,没用的话就自己找.我这个密钥是VM11 ...
- WPF读写config配置文件
1. 在你的工程中,添加app.config文件.文件的内容默认为: 1 <?xml version="1.0" encoding="utf-8" ?&g ...
- Android 使用NDK编译sipdroid Library
sipdroid是一款开源的运行于Android平台上的voip,目前支持音频和视频通话: 项目拖管地址:http://code.google.com/p/sipdroid/ 下载源代码,导入ecli ...
- Git使用总结
一.Git的特性 Speed 速度(git是用c语言写的.一般都是提交到本地) Simple design Strong support for non-linear development (tho ...
- php 使用zendstudio 生成webservice文件 wsdl
首先新建一个项目 在项目中新建下面这些文件 php类文件 test.php <?php class test { public function __construct() { } public ...
- [OpenCV] Image Processing - Fuzzy Set
使用模糊技术进行 (灰度变换Grayscale Transform) 和 (空间滤波Spatial Filtering) 模糊集合为处理不严密信息提供了一种形式. 首先,需要将输入量折算为隶属度,这个 ...
- Nodejs建站笔记-注册登录流程的简单实现
1. 使用Backbone实现前端hash路由 登录注册页面如下: 初步设想将注册和登录作为两个不同的url实现,但登录和注册功能的差距只有form表单部分,用两个url实现显然开销过大,所以最终方案 ...