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

标签 scala functional-programming state monads

当我学习 State Monad ,我不知道如何组合两个不同的函数 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理解,但不确定这是否可能

最佳答案

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

请记住,for 理解可以转换为 flatMaps 的组合。 , withFilter和一个 map .如果我们查看您的 State.flatMap ,它需要一个函数f更改 State[S,A]进入 State[S, B] .我们可以使用flatMapmap (因此是 for 理解)将同一状态上的操作链接在一起,但我们无法更改此链中状态的类型。

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

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

  • myAction我们首先使用结果n2创建 State[Int, String] ( AppendBang[String]State[S2, B] )使用第二个函数( appendBangf ),然后我们调用 runState .但是我们的结果n2String 类型( 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 中使用此功能类,您可以将其创建为外部函数:
    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 理解,因为状态的类型保持不变( (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 !!!)
    

    关于scala - 如何组成两个不同的 `State Monad` ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32410919/

    相关文章:

    map - F# - 像对待 map 一样对待函数

    .net - F# 中的业务线应用程序多久会成为常态?

    scala - SBT:插件依赖项和项目类路径

    scala - 特征中的抽象字段(Scala)-构造顺序

    scala - 为什么不应该在函数式编程中使用变量赋值

    javascript - 使用 ReactJS 通过 Redux 进行简单的状态更新事件?

    reactjs - useSelector 在初始页面加载时未接收状态,破坏了 Material ui 表

    scala - Spark MLib Word2Vec 错误 : The vocabulary size should be > 0

    haskell - 如何使用 map 制作部分应用功能的列表?

    python - globals() 与 locals() 可变性