纯函数式状态(2)

在上一个章节中我们完成如下组合子:unit,map,map2,flatMap,sequence和traverse。这些组合子并不是为处理随机状态而存在的。他们都是处理状态的通用函数,不仅仅可以处理随机状态,也可以处理其他的状态。例如我们可以给map换一个方法签名:

  def map[A, B, S](fa: S => (A, S))(f: A => B): S => (B, S) =
    s => {
      val (a, s1) = fa(s)
      (f(a), s1)
    }

State也可以换成一个更加通用的函数签名:

type State[S, +A] = S => (A, S)

这里将State定义成case class会更加方便操作,代码如下:

package org.fp.scala.state

case class State[S, +A](run: S => (A, S)) {

}

有了State之后我们可以重写使用类型别名来定义Rand:

  type Rand[+A] = State[RNG, A]

好了既然我们已经定义了一种更加通用的状态行为数据结构,那么我们为这个数据结构添加unit,map,map2,flatMap,sequence和Traverse等组合子:

package org.fp.scala.state

case class State[S, +A](run: S => (A, S)) {
  import State._

  def map[B](f: A => B): State[S, B] =
    State {s =>
      val (a, s1) = run(s)
      (f(a), s1)
    }

  def map2[B, C](sb: State[S, B])(f: (A, B) => C): State[S, C] =
    State { s =>
      val (a, s1) = run(s)
      val (b, s2) = sb.run(s1)
      (f(a, b), s2)
    }

  def flatMap[B](f: A => State[S, B]): State[S, B] =
    State { s =>
      val (a, s1) = run(s)
      f(a).run(s1)
    }

  def map_1[B](f: A => B): State[S, B] =
    flatMap(a => unit(f(a)))
  
  def map2_1[B, C](sb: State[S, B])(f: (A, B) => C): State[S, C] =
    flatMap { a =>
      sb.map { b =>
        f(a, b)
      }
    }
}

object State {

  type Rand[+A] = State[RNG, A]

  def unit[S, A](a: A): State[S, A] =
    State((a, _))

  def sequence[S, A](li: List[State[S, A]]): State[S, List[A]] =
    li match {
      case Nil => unit(Nil)
      case h :: t => h.map2(sequence(li))(_ :: _)
    }

  def sequence1[S, A](li: List[State[S, A]]): State[S, List[A]] =
    li.foldRight(unit(Nil): State[S, List[A]])((a, b) => a.map2(b)(_ :: _))
  
  def sequence2[S, A](li: List[State[S, A]]): State[S, List[A]] =
    traverse(li)(x => x)

  def traverse[S, A, B](li: List[A])(f: A => State[S, B]): State[S, List[B]] =
    li match {
      case Nil => unit(Nil)
      case h :: t => f(h).map2(traverse(t)(f))(_ :: _)
    }

  def traverse1[S, A, B](li: List[A])(f: A => State[S, B]): State[S, List[B]] =
    li.foldRight(unit(Nil): State[S, List[B]])((a, b) => f(a).map2(b)(_ :: _))
  
}

有了State和上面这些组合子之后,我们可以尝试纯函数式命令编程了。所谓命令式编程指的是程序是由一串串指令(statement)组成的,每个指令都可以修改程序的状态。在函数式编程中,指令是一个状态行为(state action),他是一个真正的函数,他接收一个状态,产生值和下一个状态。还记得前面章节介绍的for表达式么? 也就是flatMap和map的语法糖。利用for表达式我们可以更加清晰的描述命令式编程,想一想我们如何来修改State中的状态,在命令式的代码中我们通常通过set和get来修改一个对象或者程序的状态。那么对于State同样也是这样处理的,我们可通过set写入一个状态,然后通过modify修改状态,最后再通过get获取修改后的状态。来看下代码:

  def set[S](s: S): State[S, Unit] =
    State(_ => ((), s))

  def get[S]: State[S, S] =
    State(s => (s, s))

  def modify[S](f: S => S): State[S, Unit] =
    for {
      s <- get
      _ <- set(f(s))
    } yield ()

为了加强对State的使用经验,实现一个对糖果售货机建模的有限状态机。机器有两种输入模式,一种式投入硬币;另一种式按动按钮获取糖果。他可以有一种或两种状态:锁定和非锁定。它也可以跟踪还剩多少糖果以及多少硬币。

package org.fp.scala.state

trait Input

case object Coin extends Input

case object Turn extends Input

case class Machine(lock: Boolean, candies: Int, coins: Int)

机器遵循下面的规则:

  • 对于一个锁定状态的糖果机投入一枚硬币,如果有糖果,他将变为非锁定状态
  • 对于一个非锁定状态的糖果机按下按钮,他将返回糖果并返回锁定状态
  • 对于一个锁定状态的糖果机按下按钮或者一个非锁定状态的糖果机投入硬币,则什么也不做
  • 糖果机在输出糖果时忽略所有的输入
    simulateMachine方法根据输入列表返回硬币数和糖果机里最终剩余的糖果数。

下面来分析下如何处理这个问题:方法的输入是List[Input],方法的返回值是State[Machine, (Int, Int)],其中(Int, Int)为当前Machine中的糖果数和硬币数,可以表示为(machine.condies, machine.coins)。List[Input]需要转化成List[State[Machine, Unit]], 已知(Input, Machine) => Machine,那么可以通过modify可以将input转化成State[Machine, Unit], 加map就可以将List[Input]转化成List[State[Machine, Unit]]。而在之前的组合子中,sequence可以将List[State[Machine, Unit]]转换成State[Machine, List[Unit]]。最后我们只需要通过get获取machine最后的状态并map成(machine.condies, machine.coins)。类型变化如下:

(Input, Machine) => Machine
Input => Machine => State[Machine, Unit]
List[Input] => List[State[Machine, Unit]]
List[State[Machine, Unit]] => State[Machine, List[Unit]]
State[Machine, List[Unit]] => State[Machine, (Int, Int)]

全部代码如下:

package org.fp.scala.state

trait Input

case object Coin extends Input

case object Turn extends Input

case class Machine(lock: Boolean, candies: Int, coins: Int)

object Machine {

  import State._

  def simulateMachine(inputs: List[Input]): State[Machine, (Int, Int)] = {
    val li = sequence(inputs.map(i => modify(s => calculate(i, s))))
    for {
      _ <- li
      s <- get
    } yield (s.candies, s.coins)
  }

  // 对于一个锁定状态的糖果机投入一枚硬币,如果有糖果,他将变为非锁定状态
  // 对于一个非锁定状态的糖果机按下按钮,他将返回糖果并返回锁定状态
  // 对于一个锁定状态的糖果机按下按钮或者一个非锁定状态的糖果机投入硬币,则什么也不做
  // 糖果机在输出糖果时忽略所有的输入
  def calculate(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, candies, coins)) => Machine(false, candies, coins + 1)
      case (Turn, Machine(false, candies, coins)) => Machine(true, candies - 1, coins)
    }


  def main(args: Array[String]): Unit = {
    val inputs = List(Coin, Turn, Coin, Turn, Turn, Coin, Coin, Coin, Turn)
    val res = simulateMachine(inputs).run(Machine(true, 3, 0))
    print("res: " + res)
  }
}

写到这里我们知道写好纯函数式程序的诀窍:写一个函数接受一个状态,然后返回一个新的状态。利用组合子将可以避免显示传递和修改状态。下面我们将开启新的篇章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342