纯函数应该如何来处理状态?我们可以先从生成随机数这个例子开始下手,先来看下如何以副作用的方式来生成随机数:
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)
}
}
至此随机数相关的状态行为数据结构已经介绍完了, 下一章节我们会学习一种更加通用的状态行为数据结构。