如何组成两个不同的“州Monad”?

时间:2015-09-05 08:12:42

标签: scala functional-programming state monads

当我了解State Monad时,我不确定如何使用不同的State返回类型编写两个函数。

State Monad定义:

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

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

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

}

两种不同的州类型:

type AppendBang[A] = State[Int, A]

type AddOne[A] = State[String, A]

两种具有不同状态返回类型的方法:

def addOne(n: Int): AddOne[Int] = State(s => (s + ".", n + 1))

def appendBang(str: String): AppendBang[String] = State(s => (s + 1, str + " !!!"))

定义一个函数以使用上述两个函数:

def myAction(n: Int) = for {
  a <- addOne(n)
  b <- appendBang(a.toString)
} yield (a, b)

我希望像这样使用它:

println(myAction(1))

问题是myAction无法编译,它报告了一些错误:

Error:(14, 7) type mismatch;
 found   : state_monad.State[Int,(Int, String)]
 required: state_monad.State[String,?]
    b <- appendBang(a.toString)
      ^

我该如何解决?我是否必须定义一些Monad变换器?

更新:问题可能不明确,让我举个例子

我想要定义另一个在内部使用addOneappendBang的函数。既然他们都需要现有的状态,我必须传递一些:

def myAction(n: Int)(addOneState: String, appendBangState: Int): ((String, Int), String) = {
  val (addOneState2, n2) = addOne(n).runState(addOneState)
  val (appendBangState2, n3) = appendBang(n2.toString).runState(appendBangState)
  ((addOneState2, appendBangState2), n3)
}

我必须逐个运行addOneappendBang,手动传递和获取状态和结果。

虽然我发现它可以返回另一个State,但代码没有太大改进:

def myAction(n: Int): State[(String, Int), String] = State {  
case (addOneState: String, appendBangState: Int) =>  
  val (addOneState2, n2) = addOne(n).runState(addOneState)  
  val (appendBangState2, n3) = appendBang(n2.toString).runState(  appendBangState)
    ((addOneState2, appendBangState2), n3)
}

由于我不熟悉它们,只是想知道有没有办法改进它。最好的希望是我可以使用for理解,但不确定是否可能

1 个答案:

答案 0 :(得分:4)

就像我在第一篇评论中提到的那样,使用理解来做你想做的事是不可能的,因为它不能改变状态的类型(S)。

请记住,理解可以转换为flatMapswithFilter和一个map的组合。如果我们查看您的State.flatMap,则需要一个函数f才能将State[S,A]更改为State[S, B]。我们可以使用flatMapmap(以便进行理解)将操作链接到同一个状态,但我们无法更改此链中的状态类型。

我们可以概括你的myAction的最后一个定义,以使用不同类型的状态组合,组合......两个函数。我们可以尝试直接在我们的State类中实现这种通用的compose方法(尽管这可能是如此具体,它可能不属于State)。如果我们查看State.flatMapmyAction,我们可以看到一些相似之处:

  • 我们首先在现有runState实例上致电State
  • 然后我们再次致电runState

myAction中,我们首先使用结果n2使用第二个函数(State[Int, String]创建AppendBang[String]State[S2, B]appendBang)或f),然后我们称之为runState。但我们的结果n2的类型为StringA),而我们的函数appendBang需要IntB),因此我们需要一个函数将A转换为B

case class State[S, A](runState: S => (S, A)) {
  // flatMap and map

  def compose[B, S2](f: B => State[S2, B], convert: A => B) : State[(S, S2), B] =
    State( ((s: S, s2: S2) => {
      val (sNext, a) = runState(s)
      val (s2Next, b) = f(convert(a)).runState(s2)
      ((sNext, s2Next), b)
    }).tupled)
}

然后,您可以将myAction定义为:

def myAction(i: Int) = addOne(i).compose(appendBang, _.toString)

val twoStates = myAction(1)
// State[(String, Int),String] = State(<function1>)

twoStates.runState(("", 1))
// ((String, Int), String) = ((.,2),2 !!!)

如果您不想在State课程中使用此功能,可以将其创建为外部功能:

def combineStateFunctions[S1, S2, A, B](
  a: A => State[S1, A], 
  b: B => State[S2, B], 
  convert: A => B
)(input: A): State[(S1, S2), B] = State( 
  ((s1: S1, s2: S2) => {
    val (s1Next, temp) = a(input).runState(s1)
    val (s2Next, result) = b(convert(temp)).runState(s2)
    ((s1Next, s2Next), result)
  }).tupled
)

def myAction(i: Int) = 
  combineStateFunctions(addOne, appendBang, (_: Int).toString)(i)

修改: Bergi 创建两个函数,将State[A, X]State[B, X]提升为State[(A, B), X]

object State {  
  def onFirst[A, B, X](s: State[A, X]): State[(A, B), X] = {
    val runState = (a: A, b: B) => {
      val (nextA, x) = s.runState(a)
      ((nextA, b), x)
    }
    State(runState.tupled)
  }

  def onSecond[A, B, X](s: State[B, X]): State[(A, B), X] = {
    val runState = (a: A, b: B) => {
      val (nextB, x) = s.runState(b)
      ((a, nextB), x)
    }
    State(runState.tupled)
  }
}

这样你可以使用for comprehension,因为状态的类型保持不变((A, B))。

def myAction(i: Int) = for {
  x <- State.onFirst(addOne(i))
  y <- State.onSecond(appendBang(x.toString))
} yield y

myAction(1).runState(("", 1))
// ((String, Int), String) = ((.,2),2 !!!)