纯函数式状态(1)

纯函数应该如何来处理状态?我们可以先从生成随机数这个例子开始下手,先来看下如何以副作用的方式来生成随机数:

scala> val rng = new scala.util.Random
rng: scala.util.Random = scala.util.Random@63c9017b

scala> rng.nextDouble
res19: Double = 0.23321789489393963

scala> rng.nextDouble
res21: Double = 0.3633655083289712

scala> rng.nextInt
res22: Int = 928842046

scala> rng.nextInt(10)
res23: Int = 1

从上面的代码可以推断出rng对象中一定维护着一个状态,每次调用next相关产生随机数的方法都会更新状态。因为更新状态是以副作用方式来处理的,导致这些方法不是引用透明的,这意味着他们是难以测试,组合,模块化和并行化的。那么纯函数式的随机生成器式什么样子呢? 我们下看下下面的代码:

package org.fp.scala.state

trait RNG {

  def nextInt: (Int, RNG)
}

case class SimpleRNG(seed: Long) extends RNG {

  override def nextInt: (Int, RNG) = {
      val seed2 = (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)
      ((seed2 >>> 16).toInt, SimpleRNG(seed2))
    }
}

看看新的SimpleRNG, 他最大的特点就是nextInt的返回值不是Int, 而是一个类型为(Int, RNG)的元组,他包含rng产生的随机数也包含用于产生下一个随机数的rng,也就是说只要rng相同,产生的随机数就是相同的。这也就意味着我们不会在SimpleRNG保存状态,来不断更新状态来获取随机数,我们的策略是将状态作为返回值的一部分,用于下一次产生随机数。现在来看下下面的代码:

  //实现randomPair, 生成一个随机元组
  def randomPair(rng: RNG): ((Int, Int), RNG) = {
    val (i1, rng1) = rng.nextInt
    val (i2, rng2) = rng1.nextInt
    ((i1, i2), rng2)
  }

上面代码有一个非常重要的一点就是生成i2的时候我们用的是rng1而不是rng,这样避免了i1和i2总是相同的,同样最后我们把rng2作为元组的一部分返回,这可以用作生成下一个随机元组。我们再来看一组练习:

  //实现intDouble
  def intDouble(rng: RNG): ((Int, Double), RNG) = {
    val (i, rng1) = rng.nextInt
    val (d, rng2) = nextDouble(rng1)
    ((i, d), rng2)
  }

  //实现doubleInt
  def doubleInt(rng: RNG): ((Double, Int), RNG) = {
    val (d, rng1) = nextDouble(rng)
    val (i, rng2) = rng1.nextInt
    ((d, i), rng2)
  }

  //实现double3
  def double3(rng: RNG): ((Double, Double, Double), RNG) = {
    val (d1, rng1) = nextDouble(rng)
    val (d2, rng2) = nextDouble(rng1)
    val (d3, rng3) = nextDouble(rng2)
    ((d1, d2, d3), rng3)
  }

从上面的代码我们可以看到只要需要, 可以一直生成需要的值和下一次需要用的状态,然后利用他去生成下一个值。既然这样我们当然可以利用rng生成一组随机数, 来看下代码:

  //实现ints, 生成一组随机数
  def ints(n: Int, rng: RNG): (List[Int], RNG) = {
    @scala.annotation.tailrec
    def help(n: Int, li: List[Int], rng: RNG): (List[Int], RNG) =
      if (n <=0 ) (Nil, rng)
      else {
        val (i, rng1) = rng.nextInt
        help(n - 1, i :: li, rng1)
      }
    help(n, Nil, rng)
  }

回顾前面的方法签名会发现一种固定模式就是RNG => (A, RNG),这种类型的函数被称为状态行为(state action)或者叫状态转移,状态行为也可以通过组合子来组合,组合子是一个高阶函数, 我们可以利用组合子来实现自动传递状态。上面的函数类型我们可以使用一个类型别名来定义,代码如下:

  type Rand[+A] = RNG => (A, RNG)

这里一定要清楚,这类Rand[A]是一个函数类型,其实质是Function1[RNG, Tuple2[A, RNG]],现在可以使用Rand[A]重新定义nextInt,代码如下:

  val int: Rand[Int] = _.nextInt

注意这里int是函数类型,假如我们需要得到一个随机数,需要调用int(rng),或者说是int.apply(rng)。正如前面所说我们可以利用组合子来传递状态,这样的话就不需要显示的传递状态了, 现在我们来为Rand来行来添加一些组合子吧:

  def unit[A](a: A): Rand[A] = rng => (a, rng)

  def map[A, B](ra: Rand[A])(f: A => B): Rand[B] =
    rng => {
      val (a, rng2) = ra(rng)
      (f(a), rng2)
    }

有了这些组合子,我们可以利用这些组合子来实现之前定义的方法而不用显示传递状态。来看下面的练习:

  //实现noNegativeInt
  def noNegativeInt(rng: RNG): (Int, RNG) = {
    val (i, rng1) = rng.nextInt
    if (i == Int.MaxValue) (Int.MaxValue, rng1)
    else (i.abs, rng1)
  }

  def noNegativeInt1: Rand[Int] =
    map(int)(i => if (i == Int.MaxValue) Int.MaxValue else i.abs)

  //实现nextDouble
  def nextDouble(rng: RNG): (Double, RNG) = {
    val (i, rng1) = rng.nextInt
    if (i == Int.MaxValue) (1.0, rng1)
    else (i.abs / Int.MaxValue, rng1)
  }

  def nextDouble1: Rand[Double] =
    map(int)(i => if (i == Int.MaxValue) 1.0 else i.abs / Int.MaxValue)

从上的代码对比可以知道,使用了map方法在实现noNegativeInt和nextDouble的时候我们不必再显示的传递状态,只需要关注方法本身的逻辑就好了,传递状态的模式代码再map方法中已经处理了。map现在处理的能力还很有限,只能处理一个Rand,假如我们需要两个Rand呢? 先来看两一个组合子:

  def map2[A, B, C](ra: Rand[A], rb: Rand[B])(f: (A, B) => C): Rand[C] =
    rng => {
      val (a, rng2) = ra(rng)
      val (b, rng3) = rb(rng2)
      (f(a, b), rng3)
    }

有了map2之后我们可以同时处理两个Rand了,现在利用map2来改造之前的代码吧:

  //实现intDouble
  def intDouble(rng: RNG): ((Int, Double), RNG) = {
    val (i, rng1) = rng.nextInt
    val (d, rng2) = nextDouble(rng1)
    ((i, d), rng2)
  }

  def intDouble2: Rand[(Int, Double)] =
    map2(int, nextDouble1)((_, _))

  //实现doubleInt
  def doubleInt(rng: RNG): ((Double, Int), RNG) = {
    val (d, rng1) = nextDouble(rng)
    val (i, rng2) = rng1.nextInt
    ((d, i), rng2)
  }

  def doubleInt2: Rand[(Double, Int)] =
    map2(nextDouble1, int)((_, _))

从上面的代码可以看出intDouble2和doubleInt2的实现代码结构是非常类似, 那么是否可以再抽象出一个更加通用的方法呢? 当然可以,看代码:

  def both[A, B](ra: Rand[A], rb: Rand[B]): Rand[(A, B)] =
    map2(ra, rb)((_, _))

  def intDouble3: Rand[(Int, Double)] =
    both(int, nextDouble1)

  def doubleInt3: Rand[(Double, Int)] =
    both(nextDouble1, int)

既然可以利用map2组合两个Rand,而不用显示的传递RNG。那么是否有组合子可以处理一串Rand(List[Rand[A]])呢? 想想之前我们处理的Option,Either等数据结构,我i们有实现方法sequence和traverse来专门处理List[Option[A]] => Option[List[A]]和List[Either[E, A]] => Either[E, List[A]],同样Rand也需要同样的组合子:

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

  def sequence1[A](li: List[Rand[A]]): Rand[List[A]] =
    li.foldRight(unit(List.empty[A]))((a, b) => map2(a, b)(_ :: _))

  def sequence3[A](li: List[Rand[A]]): Rand[List[A]] =
    traverse(li)(x => x)

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

  def traverse1[A, B](li: List[A])(f: A => Rand[B]): Rand[List[B]] =
    li.foldRight(unit(List.empty[B]))((a, b) => map2(f(a), b)(_ :: _))

有了sequence和traverse方法后我们就可以重写ints方法,不用再显示传递状态了,代码如下:

  //实现ints, 生成一组随机数
  def ints(n: Int, rng: RNG): (List[Int], RNG) = {
    @scala.annotation.tailrec
    def help(n: Int, li: List[Int], rng: RNG): (List[Int], RNG) =
      if (n <=0 ) (Nil, rng)
      else {
        val (i, rng1) = rng.nextInt
        help(n - 1, i :: li, rng1)
      }
    help(n, Nil, rng)
  }

  def ints1(n: Int): Rand[List[Int]] = {
    val li = List.fill(n)(int)
    sequence(li)
  }

  def ints2(n: Int): Rand[List[Int]] =
    traverse((1 to n).toList)(_ => int)

好了,现在我们已经知道优雅的处理一串Rand[A],不用再显示的传递状态。那么是不是有了这些组合子之后就可以处理所有的情况了呢? 和前面的章节一样,我们还需要一个组合子flatMap,代码如下:

  def flatMap[A, B](ra: Rand[A])(f: A => Rand[B]): Rand[B] =
    rng => {
      val (a, rng1) = ra(rng)
      f(a)(rng1)
    }

flatMap比map和map2更强大,我们试着使用flatMap来实现map和map2,代码如下:

  def map_1[A, B](ra: Rand[A])(f: A => B): Rand[B] =
    flatMap(ra)(a => unit(f(a)))

  def map2_2[A, B, C](ra: Rand[A], rb: Rand[B])(f: (A, B) => C): Rand[C] =
    flatMap(ra) { a =>
      map(rb) { b =>
        f(a, b)
      }
    }

至此随机数相关的状态行为数据结构已经介绍完了, 下一章节我们会学习一种更加通用的状态行为数据结构。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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