泛函编程(17)-泛函状态-State In Action

简介:

   对OOP编程人员来说,泛函状态State是一种全新的数据类型。我们在上节做了些介绍,在这节我们讨论一下State类型的应用:用一个具体的例子来示范如何使用State类型。以下是这个例子的具体描述:

模拟一个自动糖果贩售机逻辑:贩售机有两种操作方法:投入硬币和扭动出糖旋钮。贩售机可以处于锁定和放开两种状态。模拟运作跟踪贩售机内当前的糖果和硬币数量。贩售机的操作逻辑要求如下:

1、如果机内有糖的话,投入硬币贩售机从锁定状态进入放开状态

2、在放开状态下扭动旋钮贩售机放出一块糖果后自动进入锁定状态

3、在锁定状态下扭动旋钮贩售机不做反应

4、在放开状态下投入硬币贩售机不做反应

5、没有糖果的贩售机对任何操作都不做反应

我们先把涉及到的数据类型设计出来:


1   type candy = Int    //方便表达
2   type coin = Int     //方便表达
3   sealed trait Input
4   case object Coin extends Input  //投币
5   case object Turn extends Input  //旋钮
6   case class Machine(locked: Boolean, candies: candy, coins: coin) //状态类

Machine类型就是需要变迁的状态,状态内容包括锁定状态locked,当前糖果数candies,当前硬币数coins。

我们的模拟函数款式如下:


1  def simulateMachine(inputs: List[Input]): State[Machine,coin]

输入一个操作动作List,返回State实例,值是当前硬币数,状态是Machine,里面包括了完成操作后的锁定状态、硬币数、糖果数。

我们先做一个比较直接容易明白的版本。首先是贩售机互动逻辑部分:这部分模拟了操作互动流程并在运行过程中对状态进行变迁,最终以输出形式返回最新状态:


1       def transition(input: Input, machine: Machine): Machine = {
2           (input, machine) match {
3             case (_, Machine(_,0,_)) => machine
4             case (Turn, Machine(true,_,_)) => machine
5             case (Coin, Machine(false,_,_)) => machine
6               case (Coin, Machine(true, _, nCoin)) => machine.copy(locked = false, coins = nCoin + 1)
7               case (Turn, Machine(false, nCandy, _))  => machine.copy(locked = true, candies = nCandy - 1)
8           }
9       }

这个transition函数采用了泛函状态维护风格:传入一个状态;返回新状态。流程逻辑部分是通过分析操作动作及贩售机当前状态来决定如何更新状态;一切按照设计方案要求进行。状态machine是个case class实例,这样我们可以使用machine.copy来复制一个新的状态,内容包括locked,candies,coins、又或者在某些情况下,原封不动地返回传入的状态。很明显,transition就是我们需要的状态行为函数,只要嵌入一个State实例就可以随时实现状态变迁了。

transition只能处理单个操作动作,那么如果我们输入一个List的操作动作该如何连续处理呢?既然涉及List,自然想到用递归算法应该能行:


1       def execute(inputs: List[Input], machine: Machine): Machine = {
2           inputs match {
3               case Nil => machine
4               case h::t => execute(t, transition(h,machine))
5               
6           }
7       }

在execute函数里我们对List里每个操作元素进行transition运算,状态machine也在一连串的transition运算中自动更新了。有了这两个函数我们就很容易推断出整体流程了:获取初始状态 >>> 以此初始状态输入操作处理流程并把最终结果设定为当前状态 >>> 读取当前状态:


1       for {
2           s0 <- getState    //读取起始状态
3           _ <- setState(execute(inputs,s0))  //人工设定新状态
4           s1 <- getState    //读取当前状态
5       } yield s1.coins

整个大流程还是比较容易理解的。我们注意到状态变迁采用了临时手工设定方式 setState。整个程序代码和运行结果示范如下:


 1  type candy = Int    //方便表达
 2   type coin = Int     //方便表达
 3   sealed trait Input
 4   case object Coin extends Input
 5   case object Turn extends Input
 6   case class Machine(locked: Boolean, candies: candy, coins: coin)
 7   
 8   def simulateMachine(inputs: List[Input]): State[Machine,coin] = {
 9       def transition(input: Input, machine: Machine): Machine = {
10           (input, machine) match {
11             case (_, Machine(_,0,_)) => machine
12             case (Turn, Machine(true,_,_)) => machine
13             case (Coin, Machine(false,_,_)) => machine
14               case (Coin, Machine(true, _, nCoin)) => machine.copy(locked = false, coins = nCoin + 1)
15               case (Turn, Machine(false, nCandy, _))  => machine.copy(locked = true, candies = nCandy - 1)
16           }
17       }
18        def execute(inputs: List[Input], machine: Machine): Machine = {
19           inputs match {
20               case Nil => machine
21               case h::t => execute(t, transition(h,machine))
22               
23           }
24       }
25       for {
26           s0 <- getState    //读取起始状态
27           _ <- setState(execute(inputs,s0))  //人工设定新状态
28           s1 <- getState    //读取当前状态
29       } yield s1.coins
30   }                                               //> simulateMachine: (inputs: List[ch6.state.Input])ch6.state.State[ch6.state.M
31                                                   //| achine,ch6.state.coin]
32 
33 val inputs = List(Coin, Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn)
34                                                   //> inputs  : List[Product with Serializable with ch6.state.Input] = List(Coin,
35                                                   //|  Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn)
36 simulateMachine(inputs).run(Machine(true,3,0))    //> res0: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3))
37  

以上这段代码考虑到了OOP编程人员的思维模式,采用了分段表达方式使整个程序变得更容易理解。对比起来,下面的例子就可以说是真正的泛函编程风格了。同样针对以上的贩售机模拟逻辑要求,我们将用典型的泛函风格来编程。我们先看看下面两个函数:


 1     def modify[S](f: S => S): State[S,Unit] = {
 2       for {
 3             s0 <- getState
 4             _ <- setState(f(s0))
 5         } yield ()
 6     }
 7     def sequence[S,A](xs: List[State[S,A]]): State[S,List[A]] = {
 8         def go(s: S, actList: List[State[S,A]], acc: List[A]): (List[A],S) = {
 9             actList match {
10                     case Nil => (acc.reverse, s)  //纠正排序
11                     case h::t => h.run(s) match {case (a2,s2) => go(s2,t,a2 :: acc) }
12             }
13         }
14         State(s => go(s,xs,List()))
15     }

  modify比较直白:取出当前状态,对这个状态进行变迁后再设成当前状态。sequence稍微复杂一点。我们先从它的类型匹配开始分析:接收一个List[State]、输出State[List],换句话说就是把一连串的状态变成一个状态内的一连串值。这不刚好和我们模拟函数要求匹配吗?我们要求一个函数对一连串的操作动作进行处理后产生一个最终的状态。sequence函数内部已经包含了处理循环,我们不需要execute函数了。但是这个版本的sequence函数比较低级:我是指它使用了递归算法,必须在函数内部实现状态行为run(s)。如果我们用高价一点的函数实现sequence,有可能不需要理会run(s)了:


1     //用右折叠:输入与输出同排序,但不是tail recursive
2     def sequenceByRight[S,A](xs: List[State[S,A]]): State[S,List[A]] = {
3         xs.foldRight(unit[S,List[A]](List())){ (f,acc) => f.map2(acc)(_ :: _) }
4     }
5     //用左折叠:输入与输出反排序,是tail recursive
6         def sequenceByLeft[S,A](l: List[State[S, A]]): State[S, List[A]] = { 
7           l.reverse.foldLeft(unit[S, List[A]](List()))((acc, f) => f.map2(acc)( _ :: _ ))
8         }

无论用左右折叠算法都可以实现sequence功能。注意:我们没有使用run(s),因为这个东西是在flatMap里,而map2是用flatMap实现的。用这种高阶函数使程序更加简洁。


 1 def simulateMachineFP(inputs: List[Input]): State[Machine,coin] = {
 2   for {
 3     _ <- sequence{
 4         inputs.map {
 5             input => modify {
 6                 machine: Machine => {
 7                     (input, machine) match {
 8                             case (_, Machine(_,0,_)) => machine
 9                             case (Turn, Machine(true,_,_)) => machine
10                              case (Coin, Machine(false,_,_)) => machine
11                           case (Coin, Machine(true, nCandy, nCoin)) => Machine(false,nCandy, nCoin+1)  
12                           case (Turn, Machine(false, nCandy, nCoin))  => Machine(true, nCandy - 1, nCoin) 
13                     }
14                 }
15               }
16         }
17          }
18     s <- getState
19    } yield s.coins
20 }                                                 //> simulateMachineFP: (inputs: List[ch6.state.Input])ch6.state.State[ch6.state
21                                                   //| .Machine,ch6.state.coin]
22 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)来分析理解,也就是要再熟悉多点泛函编程思考模式。

 后面补充一下:如果我来选择,我会稍退一步;把逻辑部分提出来:


 1 def simulateMachineConcise(inputs: List[Input]): State[Machine,coin] = {
 2       def transition(input: Input, machine: Machine): Machine = {
 3           (input, machine) match {
 4             case (_, Machine(_,0,_)) => machine
 5             case (Turn, Machine(true,_,_)) => machine
 6             case (Coin, Machine(false,_,_)) => machine
 7               case (Coin, Machine(true, nCandy, nCoin)) => Machine(false,nCandy, nCoin+1)  
 8               case (Turn, Machine(false, nCandy, nCoin))  => Machine(true, nCandy - 1, nCoin) 
 9           }
10       }
11 
12   for {
13         _ <- sequence{inputs.map {input => modify {machine: Machine => transition(input, machine)}}}                   s <- getState
15       } yield s.coins
16 }                                                 //> simulateMachineConcise: (inputs: List[ch6.state.Input])ch6.state.State[ch6.
17                                                   //| state.Machine,ch6.state.coin]
18 simulateMachineConcise(inputs).run(Machine(true,3,0))
19                                                   //> res2: (ch6.state.coin, ch6.state.Machine) = (3,Machine(true,0,3))
20  

这段核心代码是不是简洁多了,也比较容易理解:


1   for {
2     _ <- sequence{inputs.map {input => modify {machine: Machine => transition(input, machine)}}}                              s <- getState
3       } yield s.coins

相关文章
|
2天前
|
存储 JavaScript 前端开发
JavaScript基础
本节讲解JavaScript基础核心知识:涵盖值类型与引用类型区别、typeof检测类型及局限性、===与==差异及应用场景、内置函数与对象、原型链五规则、属性查找机制、instanceof原理,以及this指向和箭头函数中this的绑定时机。重点突出类型判断、原型继承与this机制,助力深入理解JS面向对象机制。(238字)
|
1天前
|
云安全 人工智能 安全
阿里云2026云上安全健康体检正式开启
新年启程,来为云上环境做一次“深度体检”
1470 6
|
3天前
|
安全 数据可视化 网络安全
安全无小事|阿里云先知众测,为企业筑牢防线
专为企业打造的漏洞信息收集平台
1304 2
|
3天前
|
缓存 算法 关系型数据库
深入浅出分布式 ID 生成方案:从原理到业界主流实现
本文深入探讨分布式ID的生成原理与主流解决方案,解析百度UidGenerator、滴滴TinyID及美团Leaf的核心设计,涵盖Snowflake算法、号段模式与双Buffer优化,助你掌握高并发下全局唯一ID的实现精髓。
319 160
|
3天前
|
人工智能 自然语言处理 API
n8n:流程自动化、智能化利器
流程自动化助你在重复的业务流程中节省时间,可通过自然语言直接创建工作流啦。
365 4
n8n:流程自动化、智能化利器
|
11天前
|
机器学习/深度学习 安全 API
MAI-UI 开源:通用 GUI 智能体基座登顶 SOTA!
MAI-UI是通义实验室推出的全尺寸GUI智能体基座模型,原生集成用户交互、MCP工具调用与端云协同能力。支持跨App操作、模糊语义理解与主动提问澄清,通过大规模在线强化学习实现复杂任务自动化,在出行、办公等高频场景中表现卓越,已登顶ScreenSpot-Pro、MobileWorld等多项SOTA评测。
1476 7
|
5天前
|
人工智能 API 开发工具
Skills比MCP更重要?更省钱的多!Python大佬这观点老金测了一周终于懂了
加我进AI学习群,公众号右下角“联系方式”。文末有老金开源知识库·全免费。本文详解Claude Skills为何比MCP更轻量高效:极简配置、按需加载、省90% token,适合多数场景。MCP仍适用于复杂集成,但日常任务首选Skills。推荐先用SKILL.md解决,再考虑协议。附实测对比与配置建议,助你提升效率,节省精力。关注老金,一起玩转AI工具。
|
1天前
|
Linux 数据库
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
本文介绍在CentOS 7.9环境下安装PolarDB-X单机版数据库的完整流程,涵盖系统环境准备、本地Yum源配置、RPM包安装、用户与目录初始化、依赖库解决、数据库启动及客户端连接等步骤,助您快速部署运行PolarDB-X。
228 1
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
|
12天前
|
人工智能 Rust 运维
这个神器让你白嫖ClaudeOpus 4.5,Gemini 3!还能接Claude Code等任意平台
加我进AI讨论学习群,公众号右下角“联系方式”文末有老金的 开源知识库地址·全免费
1342 17
|
3天前
|
自然语言处理 监控 测试技术
互联网大厂“黑话”完全破译指南
互联网大厂黑话太多听不懂?本文整理了一份“保姆级”职场黑话词典,涵盖PRD、A/B测试、WLB、埋点、灰度发布等高频术语,用大白话+生活化类比,帮你快速听懂同事在聊什么。非技术岗也能轻松理解,建议收藏防踩坑。
274 161

热门文章

最新文章