如何组成两个不同的`State Monad`?

How to compose two different `State Monad`?

当我学习 State Monad 时,我不确定如何组合具有不同 State return 类型的两个函数。

状态 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]

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

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 一个接一个,手动传递和获取状态和结果。

虽然我发现可以return另一个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 理解,但不确定是否可行

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

请记住,for comprehension 可以翻译为 flatMapswithFilter 和一个 map 的组合。如果我们查看您的 State.flatMap,它需要一个函数 f 才能将 State[S,A] 更改为 State[S, B]。我们可以使用 flatMapmap(因此是 for comprehension)将对同一状态的操作链接在一起,但我们不能更改此链中的状态类型。

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

  • 我们首先在现有的 State 实例上调用 runState
  • 然后我们再次调用 runState

myAction中我们首先使用结果n2创建一个State[Int, String]AppendBang[String]State[S2, B])使用第二个函数(appendBangf) 然后我们调用 runState。但是我们的结果 n2 是类型 String (A) 而我们的函数 appendBang 需要一个 Int (B) 所以我们需要一个函数将 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 class 中使用此函数,您可以将其创建为外部函数:

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 !!!)