Scalaz(18)- Monad: ReaderWriterState-可以是一种简单的编程语言

  说道FP,我们马上会联想到Monad。我们说过Monad的代表函数flatMap可以把两个运算F[A],F[B]连续起来,这样就可以从程序的意义上形成一种串型的流程(workflow)。更直白的讲法是:任何类型只要实现了flatMap就可以用for-comprehension, for{...}yield。在这个for{...}里我们可以好像OOP一样编写程序。这个for就是一种运算模式,它规范了在for{...}里指令的行为。我们正从OOP风格走入FP编程模式,希望有个最基本的FP编程模式使我们能够沿用OOP编程风格的语法和思维。Monad应该就是最合适的泛函数据类型了。我们先从最基本的开始:假如我们有一段行令程序:

/*
val a = e1
val b = e2(a)
val c = e3(a,b)
val d = e2(c)
*/

通过这些函数e1,e2,e3最后计算出d值。如果是用FP风格来编这段程序的话,首先我们必须把函数的结果d放入F[d]的F里。F就是上面所说的运算模式,在这里可以用大家熟悉的context(上下文)来表示。F必须是个Monad,F[]相当于for{...}yield。我们先试试用Id,虽然Id[A]对A不做任何处理,直接返回,好像没什么意义,但这种类型具备了map和flatMap,应该可以用for-comprehension:

 1 import scalaz._
 2 import Scalaz._
 3 def e1:Id[Int] = 10                               //> e1: => scalaz.Scalaz.Id[Int]
 4 def e2(a: Int): Id[Int] = a + 1                   //> e2: (a: Int)scalaz.Scalaz.Id[Int]
 5 def e3(a: Int, b: Int): Id[Int] = a + b           //> e3: (a: Int, b: Int)scalaz.Scalaz.Id[Int]
 6 for {
 7     a <- e1
 8     b <- e2(a)
 9     c <- e3(a,b)
10     d <- e2(c)
11 } yield d                                         //> res0: scalaz.Scalaz.Id[Int] = 22

可以看到,在for-loop里就是OOP的行令程序。不过如果觉着这个Id没什么意义,可以试试Option看:

 1 import scalaz._
 2 import Scalaz._
 3 def e1:Option[Int] = 10.some                      //> e1: => Option[Int]
 4 def e2(a: Int): Option[Int] = (a + 1).some        //> e2: (a: Int)Option[Int]
 5 def e3(a: Int, b: Int): Option[Int] = (a + b).some//> e3: (a: Int, b: Int)Option[Int]
 6 for {
 7     a <- e1
 8     b <- e2(a)
 9     c <- e3(a,b)
10     d <- e2(c)
11 } yield d                                         //> res0: Option[Int] = Some(22)

看,虽然换了个壳子(context), 但for-loop里的程序没有变化。换一句话讲就是for-loop里的程序根本不理会包裹的context。

Reader也是一种Monad,用它又怎样呢:

 1 import scalaz._
 2 import Scalaz._
 3 def e1:Reader[Int,Int] = Reader[Int,Int](a => a)  //> e1: => scalaz.Reader[Int,Int]
 4 def e2(a: Int): Reader[Int,Int] = Reader[Int,Int](_ => a + 1)
 5                                                   //> e2: (a: Int)scalaz.Reader[Int,Int]
 6 def e3(a: Int, b: Int): Reader[Int, Int] = Reader[Int,Int](_ => a+b)
 7                                                   //> e3: (a: Int, b: Int)scalaz.Reader[Int,Int]
 8 val prg = for {
 9     a <- e1
10     b <- e2(a)
11     c <- e3(a,b)
12     d <- e2(c)
13 } yield d                                         //> prg  : scalaz.Kleisli[scalaz.Id.Id,Int,Int] = Kleisli(<function1>)
14 prg.run(10)                                       //> res0: scalaz.Id.Id[Int] = 22

虽然在语法上有些蹩脚,但还是证明了for-loop里的程序是不理会外面context的。那么我们可不可以说这个prg就是一个简单的FP编程语言。它把运算结果放在context里,直至运行了某种interpreter才能取得实际的运算值(用run(10)得到22)。当然,一段程序,它的运算行为受制于单一种类型的context可能有些弱了。如果需要获得一种可用的FP编程语言,我们可能还是要探讨如何把单一类型context组合成多类型混合的context。

我们发现在scalaz里有些type class的名称是以T结束的如:ReaderT,WriterT,StateT等等。这个T指的是变形器Transformer,意思是用它可以堆砌(stacking)context。看看StateT,简单定义应该是这样的: 

case class StateT[F[_],S,A](run: S => F[(S,A)])

我们可以把F类堆砌在State上。实践证明如果这个F实现了flatMap,那么堆砌成的类型也能实现flatMap。好,scalaz的Option是实现了flatMap的,那么能不能把它和State堆砌在一起呢?堆砌而成的context会有什么效果呢?我们先看看单一Option和State作为一种context的效果:

 1 for {
 2   a <- 3.some
 3   b <- (None: Option[Int])
 4   c <- 4.some
 5 } yield c                                         //> res1: Option[Int] = None
 6 val statePrg = for {
 7   a <- get[Int]
 8   b <- State[Int,Int](s => (s, s + a))
 9   _ <- put(9)
10 } yield b                                         //> statePrg  : scalaz.IndexedStateT[scalaz.Id.Id,Int,Int,Int] = scalaz.IndexedS
11                                                   //| tateT$$anon$10@15ff3e9e
12 statePrg.run(3)                                   //> res2: scalaz.Id.Id[(Int, Int)] = (9,6)

依我来看,Option主要效果是在遇到None值时立即退出。而State的主要作用是在运算同时可以维护一个状态。那么如果把Option和State叠加起来就会同时具备这两种类型的特点了吧?也就是既能维护状态又能在遇到None值时立即终止运算退出了。首先验证一下用Option的flatMap来实现叠加context的flatMap:

 case class OptionState[S,A](run: S => Option[(S,A)]) {
   def map[B](f: A => B): OptionState[S,B] =
    OptionState { 
       s => run(s) map { case (s1,a1) => (s1,f(a1)) }
    }
   def flatMap[B](f: A => OptionState[S,B]): OptionState[S,B] =
    OptionState { 
       s => run(s) flatMap { case (s1,a1) => f(a1).run(s1) }
    }
 }

是的,我们可以用Option的map和flatMap来实现OptionState的map和flatMap。当然,如果我们想在一个for-comprehension里同时使用Option和State就必须把它们升格成OptionState类型:

 def liftOption[S,A](oa: Option[A]): OptionState[S,A] = oa match {
  case Some(a) => OptionState {s => (s,a).some }
  case None => OptionState {_ => none}
 }
 def liftState[S,A](sa: State[S,A]): OptionState[S,A] =
    OptionState {s => sa(s).some}

现在试试用叠加效果的for-comprehension:

 1 val osprg: OptionState[Int,Int] = for {
 2    a <- liftOption(3.some)
 3    b <- liftState(put(a))
 4    c <- liftState(get[Int])
 5    d <- liftState(State[Int,Int](s => (s+c, s+a)))
 6  } yield c                                        //> osprg  : Exercises.rws.OptionState[Int,Int] = OptionState(<function1>)
 7  osprg.run(2)                                     //> res3: Option[(Int, Int)] = Some((6,3))
 8  val osprg1: OptionState[Int,Int] = for {
 9    a <- liftOption(3.some)
10    b <- liftState(put(a))
11    _ <- liftOption((None: Option[Int]))
12    c <- liftState(get[Int])
13    d <- liftState(State[Int,Int](s => (s+c, s+a)))
14  } yield c                                        //> osprg1  : Exercises.rws.OptionState[Int,Int] = OptionState(<function1>)
15  osprg1.run(2)                                    //> res4: Option[(Int, Int)] = None

看,既可以维护状态又具备None处理机制。

好了,scalaz里有个ReaderWriterState这么个type class,就是一个Reader+Writer+State堆砌的Monad。相信scalaz特别提供了这么个type class应该有它的用意。我的猜想是这个Monad是个功能比较完整的组合Monad。作为for-comprehension的context应该能提供比较全面的效果。从字意上解释就是在由它形成的Monadic编程语言里可以同时提供运算(compute)、跟踪(logging)和状态维护功能。它的基础类型是IndexedReaderWriterStateT:scalaz/package.scala

  type ReaderWriterStateT[F[_], -R, W, S, A] = IndexedReaderWriterStateT[F, R, W, S, S, A]
  object ReaderWriterStateT extends ReaderWriterStateTInstances with ReaderWriterStateTFunctions {
    def apply[F[_], R, W, S, A](f: (R, S) => F[(W, A, S)]): ReaderWriterStateT[F, R, W, S, A] = IndexedReaderWriterStateT[F, R, W, S, S, A] { (r: R, s: S) => f(r, s) }
  }
  type IndexedReaderWriterState[-R, W, -S1, S2, A] = IndexedReaderWriterStateT[Id, R, W, S1, S2, A]
  object IndexedReaderWriterState extends ReaderWriterStateTInstances with ReaderWriterStateTFunctions {
    def apply[R, W, S1, S2, A](f: (R, S1) => (W, A, S2)): IndexedReaderWriterState[R, W, S1, S2, A] = IndexedReaderWriterStateT[Id, R, W, S1, S2, A] { (r: R, s: S1) => f(r, s) }
  }
  type ReaderWriterState[-R, W, S, A] = ReaderWriterStateT[Id, R, W, S, A]
  object ReaderWriterState extends ReaderWriterStateTInstances with ReaderWriterStateTFunctions {
    def apply[R, W, S, A](f: (R, S) => (W, A, S)): ReaderWriterState[R, W, S, A] = IndexedReaderWriterStateT[Id, R, W, S, S, A] { (r: R, s: S) => f(r, s) }
  }
  type IRWST[F[_], -R, W, -S1, S2, A] = IndexedReaderWriterStateT[F, R, W, S1, S2, A]
  val IRWST: IndexedReaderWriterStateT.type = IndexedReaderWriterStateT
  type IRWS[-R, W, -S1, S2, A] = IndexedReaderWriterState[R, W, S1, S2, A]
  val IRWS: IndexedReaderWriterState.type = IndexedReaderWriterState
  type RWST[F[_], -R, W, S, A] = ReaderWriterStateT[F, R, W, S, A]
  val RWST: ReaderWriterStateT.type = ReaderWriterStateT
  type RWS[-R, W, S, A] = ReaderWriterState[R, W, S, A]
  val RWS: ReaderWriterState.type = ReaderWriterState

如果把Reader,Writer,State款式分开来对比分析的话:

case class Reader[R, A](f: R => A)     //传入R,返回A后不理会R
case class Writer[W, A](w: (W, A))     //直接返回W,A
case class State[S, A](f: S => (A, S)) //传入S, 返回A和S

那么把以上三个结合起来后它的款式应该是这样的了吧:

case class ReaderWriterState[R, W, S, A](
          run: (R, S) => (W, A, S)  //传入R,S 返回W,A,S
) 
case class ReaderWriterStateT[F[_],R, W, S, A](
          run: (R, S) => F[(W, A, S)]  //传入R,S 返回W,A,S。只是包在了F内
) 

传入的和返回的类型是匹配的。在scalaz里是这样定义的:scalaz/ReaderWriterStateT.scala

/** A monad transformer stack yielding `(R, S1) => F[(W, A, S2)]`. */
sealed abstract class IndexedReaderWriterStateT[F[_], -R, W, -S1, S2, A] {
  self =>
  def run(r: R, s: S1): F[(W, A, S2)]

  /** Discards the writer component. */
  def state(r: R)(implicit F: Functor[F]): IndexedStateT[F, S1, S2, A] =
    IndexedStateT((s: S1) => F.map(run(r, s)) {
      case (w, a, s1) => (s1, a)
    })

  /** Calls `run` using `Monoid[S].zero` as the initial state */
  def runZero[S <: S1](r: R)(implicit S: Monoid[S]): F[(W, A, S2)] =
    run(r, S.zero)

  /** Run, discard the final state, and return the final value in the context of `F` */
  def eval(r: R, s: S1)(implicit F: Functor[F]): F[(W, A)] =
    F.map(run(r,s)) { case (w,a,s2) => (w,a) }

  /** Calls `eval` using `Monoid[S].zero` as the initial state */
  def evalZero[S <: S1](r:R)(implicit F: Functor[F], S: Monoid[S]): F[(W,A)] =
    eval(r,S.zero)

  /** Run, discard the final value, and return the final state in the context of `F` */
  def exec(r: R, s: S1)(implicit F: Functor[F]): F[(W,S2)] =
    F.map(run(r,s)){case (w,a,s2) => (w,s2)}

  /** Calls `exec` using `Monoid[S].zero` as the initial state */
  def execZero[S <: S1](r:R)(implicit F: Functor[F], S: Monoid[S]): F[(W,S2)] =
    exec(r,S.zero)
...

我们看到IndexedReaderWriterStateT已经实现了很多IndexedStateT的运算方法如:eval,exec等。看看它的map和flatMap是怎么实现的:

def map[B](f: A => B)(implicit F: Functor[F]): IndexedStateT[F, S1, S2, B] = IndexedStateT(s => F.map(apply(s)) {
    case (s1, a) => (s1, f(a))
})
def flatMap[S3, B](f: A => IndexedStateT[F, S2, S3, B])(implicit F: Bind[F]): IndexedStateT[F, S1, S3, B] = IndexedStateT(s => F.bind(apply(s)) {
    case (s1, a) => f(a)(s1)
})

与我们前面所做的OptionState例子一样:如果F能实现map和flatMap则IndexedReaderWriterStateT就能实现map和flatMap。为了省却在for-loop里每行命令都使用lift进行类型升格,IndexedReaderWriterStateT重新实现了大部分操作函数:

private trait ReaderWriterStateTMonad[F[_], R, W, S]
  extends MonadReader[({type λ[r, α]=ReaderWriterStateT[F, r, W, S, α]})#λ, R]
  with MonadState[({type f[s, α] = ReaderWriterStateT[F, R, W, s, α]})#f, S]
  with MonadListen[({type f[w, α] = ReaderWriterStateT[F, R, w, S, α]})#f, W]
  with IndexedReaderWriterStateTFunctor[F, R, W, S, S] {
  implicit def F: Monad[F]
  implicit def W: Monoid[W]

  def bind[A, B](fa: ReaderWriterStateT[F, R, W, S, A])(f: A => ReaderWriterStateT[F, R, W, S, B]): ReaderWriterStateT[F, R, W, S, B] = fa flatMap f
  def point[A](a: => A): ReaderWriterStateT[F, R, W, S, A] =
    ReaderWriterStateT((_, s) => F.point((W.zero, a, s)))
  def ask: ReaderWriterStateT[F, R, W, S, R] =
    ReaderWriterStateT((r, s) => F.point((W.zero, r, s)))
  def local[A](f: R => R)(fa: ReaderWriterStateT[F, R, W, S, A]): ReaderWriterStateT[F, R, W, S, A] =
    ReaderWriterStateT((r, s) => fa.run(f(r), s))
  override def scope[A](k: R)(fa: ReaderWriterStateT[F, R, W, S, A]): ReaderWriterStateT[F, R, W, S, A] =
    ReaderWriterStateT((_, s) => fa.run(k, s))
  override def asks[A](f: R => A): ReaderWriterStateT[F, R, W, S, A] =
    ReaderWriterStateT((r, s) => F.point((W.zero, f(r), s)))
  def init: ReaderWriterStateT[F, R, W, S, S] =
    ReaderWriterStateT((_, s) => F.point((W.zero, s, s)))
  def get = init
  def put(s: S): ReaderWriterStateT[F, R, W, S, Unit] =
    ReaderWriterStateT((r, _) => F.point((W.zero, (), s)))
  override def modify(f: S => S): ReaderWriterStateT[F, R, W, S, Unit] =
    ReaderWriterStateT((r, s) => F.point((W.zero, (), f(s))))
  override def gets[A](f: S => A): ReaderWriterStateT[F, R, W, S, A] =
    ReaderWriterStateT((_, s) => F.point((W.zero, f(s), s)))
  def writer[A](w: W, v: A): ReaderWriterStateT[F, R, W, S, A] =
    ReaderWriterStateT((_, s) => F.point((w, v, s)))
  override def tell(w: W): ReaderWriterStateT[F, R, W, S, Unit] =
    ReaderWriterStateT((_, s) => F.point((w, (), s)))
  def listen[A](ma: ReaderWriterStateT[F, R, W, S, A]): ReaderWriterStateT[F, R, W, S, (A, W)] =
    ReaderWriterStateT((r, s) => F.map(ma.run(r, s)) { case (w, a, s1) => (w, (a, w), s1)})
}

我们示范用这个ReaderWriterState来写一段程序:模拟一段通讯端口使用程序并把使用情况记录下来。先传入一个端口号,在程序中可以重设使用的端口号:

1   val program: ReaderWriterState[Config, List[String], Int, Int] = for {
2     _   <- log("Start - r: %s, s: %s")
3     res <- invokeService
4     _   <- log("Between - r: %s, s: %s")
5     _   <- setService(8,"Com8")
6     _   <- invokeService
7     _   <- log("Done - r: %s, s: %s")
8   } yield res      //> program  : scalaz.RWS[Exercises.rws.Config,List[String],Int,Int] = scalaz.I
9                    //| ndexedReaderWriterStateT$$anon$5@223191a6

这倒像是一段高级语言写的程序。细节都在几个功能函数里。它们都必须返回ReaderWriterState类型:

 1   case class Config(var port: Int, var portName: String)
 2   def log[R, S](msg: String): RWS[R, List[String], S, Unit] =
 3     ReaderWriterState {
 4       case (r, s) => (msg.format(r, s) :: Nil, (), s) //.point[Identity]
 5     }                                             //> log: [R, S](msg: String)scalaz.RWS[R,List[String],S,Unit]
 6   def invokeService: ReaderWriterState[Config, List[String], Int, Int] =
 7     ReaderWriterState {
 8       case (cfg, invocationCount) => (
 9         List("Invoking service with port: " + cfg.portName),
10         scala.util.Random.nextInt(100),
11         invocationCount + 1
12       ) //.point[Identity]
13     }                                             //> invokeService: => scalaz.ReaderWriterState[Exercises.rws.Config,List[String
14                                                   //| ],Int,Int]
15   def setService(p: Int, n: String): ReaderWriterState[Config, List[String], Int, Int] =
16     ReaderWriterState {
17       case (cfg, invocationCount) => cfg.port=p; cfg.portName=n
18         (List("Changing service port to " + cfg.portName),
19           scala.util.Random.nextInt(100),
20           invocationCount)
21     }                                             //> setService: (p: Int, n: String)scalaz.ReaderWriterState[Exercises.rws.Confi
22                                                   //| g,List[String],Int,Int]
23 
24   val program: ReaderWriterState[Config, List[String], Int, Int] = for {
25     _   <- log("Start - r: %s, s: %s")
26     res <- invokeService
27     _   <- log("Between - r: %s, s: %s")
28     _   <- setService(8,"Com8")
29     _   <- invokeService
30     _   <- log("Done - r: %s, s: %s")
31   } yield res                                     //> program  : scalaz.RWS[Exercises.rws.Config,List[String],Int,Int] = scalaz.I
32                                                   //| ndexedReaderWriterStateT$$anon$5@223191a6
33 val r =   program run (Config(443,"Com3"), 0)     //> r  : scalaz.Id.Id[(List[String], Int, Int)] = (List(Start - r: Config(443,C
34                                                   //| om3), s: 0, Invoking service with port: Com3, Between - r: Config(443,Com3)
35                                                   //| , s: 1, Changing service port to Com8, Invoking service with port: Com8, Do
36                                                   //| ne - r: Config(88,Com8), s: 2),68,2)
37 println("Result: " + r._2)                        //> Result: 68
38 println("Service invocations: " + r._3)           //> Service invocations: 2
39 println("Log: %n%s".format(r._1.mkString("\t", "%n\t".format(), "")))
40                                                   //> Log: 
41                                                   //|     Start - r: Config(443,Com3), s: 0
42                                                   //|     Invoking service with port: Com3
43                                                   //|     Between - r: Config(443,Com3), s: 1
44                                                   //|     Changing service port to Com8
45                                                   //|     Invoking service with port: Com8
46                                                   //|     Done - r: Config(88,Com8), s: 2

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java达人

switch.....case....使用最容易犯错的地方

请诸位看这段代码,如果runtimeDatabase 为MYSQL或H2,标注1的那行代码是否会执行。 final RuntimeDatabase runtim...

20210
来自专栏逸鹏说道

Python3 与 C# 扩展之~基础衍生

在线编程: https://mybinder.org/v2/gh/lotapp/BaseCode/master

823
来自专栏机器学习实践二三事

Python解析JSON

什么是JSON JSON(JavaScript Object Notation)是一种轻量级的数据交换语言,以文字为基础,且易于让人阅读。JSON是Javasc...

2277
来自专栏流柯技术学院

TestNG 三 测试方法

测试方法是可以带有参数的。每个测试方法都可以带有任意数量的参数,并且可以通过使用TestNG的@Parameters向方法传递正确的参数。

733
来自专栏向治洪

Kotlin的委托属性和区间

委托属性 委托属性 是一种通过委托实现拥有 getter 和可选 setter 的 属性,并允许实现可复用的自定义属性。例如: class Example { ...

2287
来自专栏斑斓

Scala实现DSL的框架案例

在skinny.validator框架中,提供如下的一种DSL调用方式来验证Map的值:

724
来自专栏分布式系统和大数据处理

编写高质量代码:改善C#程序的157个建议

这本书汇集了C#编写中的一些建议、注意事项和小技巧,为了温故知新,又拿出来翻看了一遍。很多主题要阐述的内容是显而易见、不言自明的,就没有再写摘要。部分主题的名称...

573
来自专栏noteless

[二十四]JavaIO之PrintWriter

他与PrintStream的逻辑上功能目的是相同的--他们都想做同一件事情--更便捷的格式化打印输出

432
来自专栏Android机动车

RxJava从入门到不离不弃(三)——转换操作符

所有这些Operators都作用于一个可观测序列,然后变换它发射的值,最后用一种新的形式返回它们。概念实在是不好理解,下面我们结合实际的例子一一介绍。

613
来自专栏一个会写诗的程序员的博客

从 Java 开始进入 Kotlin的世界: From Java to Kotlin: JKotlinor从 Java 开始进入 Kotlin的世界: From Java to Kotlin: JKo

在前面的内容里,我们已经看到了Java与Kotlin的互操作的基本方式。为了更好的认识Java与Kotlin这两门语言,我们在这里给出一些基本功能,同时使用Ja...

852

扫码关注云+社区