【问题标题】:In Scala Functional Programming, is there an idiomatic way to map with a state?在 Scala 函数式编程中,是否有一种惯用的方式来映射状态?
【发布时间】:2020-04-07 02:06:36
【问题描述】:

传统的映射函数具有签名A => B,用于将F[A] 转换为F[B],例如将List[A] 转换为List[B]

但是,如果映射函数应该携带一些计算B 所需的状态,你该怎么办?

比如说,映射函数是这样的:(A, S) => (B, S),其中 S 是 State 的类型。对于每个A,先前返回的S 被传递到映射函数,而最初为状态提供zero 元素。然后映射函数返回一个新状态(连同结果),然后再次与下一个值一起传递,以此类推。

当然,.map 不够强大,无法做到这一点,所以解决方案必须基于另一个运算符。

为了说明的目的,举一个具体的例子,假设我有一个Ints 的序列,我想计算每个Int 与该序列中前一个Int 的差异。上述映射函数的实现如下所示:

  def mapping(currentElement: Int, previousElement: Option[Int]): (Option[Int], Option[Int]) = {
    (previousElement.map(currentElement - _), Some(currentElement))
  }

previousElement 的初始 zero 值将是 None,在第一个元素之后始终是 Some(currentElement)。每次迭代的结果将是当前值减去最后一个值的Some,但第一个元素除外,它是None

如何使用mapping 函数将List(1, 4, 3) 转换为List(None, Some(3), Some(-1))

(请注意,整数减法示例仅用于说明目的,问题的重点是所述操作类型的通用解决方案。)

【问题讨论】:

  • 也许scanLeft 是你想要的。

标签: scala functional-programming


【解决方案1】:

Scala 2.13.x unfold() 方法维护一个与您的示例类似的状态。

List.unfold((Option.empty[Int], List(1, 4, 3))){
  case (prev, hd::tl) => Some((prev.map(hd.-), (Some(hd),tl)))
  case (prev, Nil)    => None
}
//res0: List[Option[Int]] = List(None, Some(3), Some(-1))

这在LazyListIterator 上可用,因此可用于创建伪无限流。

【讨论】:

    【解决方案2】:

    有一些库可用于执行您所描述的“mtl 样式”状态传递。 (检查这个sn-p之后的类型签名)

    import cats._
    import cats.data._
    import cats.implicits._
    
    // Given an element and state, calculate next state and return value
    def modifyEntry(currentElement: Int): State[Option[Int], Option[Int]] = for {
      previousElement <- State.get
      _ <- State.set[Option[Int]](Some(currentElement)) // Next State
    } yield previousElement map (currentElement - _) // Calculated Value
    
    // It is useful for hiding state and passing it implicitly
    val result =
      for {
        val1 <- modifyEntry(1)
        val2 <- modifyEntry(2)
        val3 <- modifyEntry(3)
      // Final state is implicittly stored in the yielded State[Option[Int], Seq[Option[Int]]]
      } yield Seq(val1, val2, val3) 
    
    // Run with None initial State and coerce evaluation (cats is lazy by default)
    println("for-comprehension result (final state and value): " -> result.run(None).value)
    
    // More importantly, it is _easy_ to compose with Traversables or other generic cats traits
    println("traverse result (only value): " ->
      List(1,2,3).traverse(modifyEntry).runA(None).value) // List(None, Some(1), Some(1))
    println("traverse result (only value): " ->
      List(1,4,3).traverse(modifyEntry).runA(None).value) // List(None, Some(3), Some(-1))
    

    您会对来自scalazStateFunctions 特征或来自catsState 特别感兴趣。对比:https://github.com/fosskers/scalaz-and-cats

    Scalaz 状态函数:

    trait StateFunctions extends IndexedStateFunctions {
      // ...
      def get[S]: State[S, S] = State(s => (s, s))
    
      def put[S](s: S): State[S, Unit] = State(_ => (s, ()))
      // ...
    }
    

    经过一些修改的 Cats StateFunctions:

    abstract private[data] class StateFunctions {
      // ...
      def get[S]: State[S, S] = ??? // Some other code like State(s => (s, s))
    
      def set[S](s: S): State[S, Unit] = State(_ => (s, ()))
    }
    

    对于猫,请查看带有其他示例的优秀文档:https://typelevel.org/cats/datatypes/state.html

    对于 scalaz,这里有一个关于 Scala 和 Scalaz 中“mtl-style”概述的精彩演讲:Paweł Szulc - GETTING MORE MILEAGE FROM YOUR MONADS WITH MTL,但要注意

    对于其中任何一个,请注意 MonadTransformers 的缺点(不是 mtl-style/traits,请参阅第二部分):http://degoes.net/articles/effects-without-transformers

    【讨论】:

    • 我看到了类型签名的相似性,我正在研究这个,但到目前为止我还不清楚实际应用,如何使用StateFunctionsList(1, 4, 3)List(None, Some(3), Some(-1))
    • 状态将被法线贴图(和 flatMap)保存,所以如果你想转换一个列表并保存状态,就使用它。
    • 嗨@ig-dev,我修改了答案来说明你的例子,抱歉耽搁了。
    • 还有@ig-dev,这里是你评论的产生正确输出的例子:scalafiddle.io/sf/vLgGCRF/2
    【解决方案3】:

    你要找的“运营商”是fold

    List(1, 4, 3).foldLeft(None: Option[Int], List[Option[Int]]())
       ((acc, curr) => (Some(curr), acc._1.map(_ - curr) :: acc._2))
        ._2
        .reverse
    

    另一种思考方式是使用zip

    val xs = List(1, 4, 3)
    
    val result = None :: xs.zip(xs.drop(1)).map(currAndNext => Some(currAndNext._2 - currAndNext._1))
    

    【讨论】:

    • 这很好地回答了这个例子,但它只适用于有限的数据结构。如果 F[A] 是无限流(这是我需要的),它将不起作用
    • @ig-dev 取决于您用于流的内容。我相信 fs2 可以解决这个问题
    • @sinanspd 是吗?我不这么认为,因为折叠不会返回中间结果。 fs2 有这个机制吗?
    • @ig-dev 我已经有一段时间没有研究 fs2 内部了,所以请仔细检查一下,但据我记得,fs2 折叠被定义为拉动和折叠。所以它从 Stream 中提取新数据,然后折叠。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-21
    • 1970-01-01
    • 2016-10-12
    • 2012-10-09
    • 1970-01-01
    • 2019-03-22
    相关资源
    最近更新 更多