如何使用猫和状态单子

Posted

技术标签:

【中文标题】如何使用猫和状态单子【英文标题】:How to use cats and State Monad 【发布时间】:2017-05-15 09:58:36 【问题描述】:

我第一次使用cats 来解决代码出现的day 1,我想知道是否有可能改进。

给定一个具有以下签名的方法update def update(i: Instruction): PosAndDir => PosAndDir

我想出了:

val state: State[PosAndDir, List[Unit]] = instructions.map(i => State.modify(update(i))).toList.sequenceU
val finalState = state.runS(PosAndDir(Pos(0, 0), North)).value

还有

  def update2(i: Instruction): State[PosAndDir, Option[Pos]] =
    State.modify(update(i)).inspect(pad => if (i == Walk) Some(pad.pos) else None)
  …
  val state = instructions.map(update2).toList.sequenceU
  val positions = state.runA(PosAndDir(Pos(0, 0), North)).value.flatten

更准确地说,问题是:

    为什么我们需要调用.value(使用scalaz,它是透明的)? 有没有办法写update2 以提高可读性? 猫中有SeqApplicative 实例(我知道scalaz 中没有)。 ? 有改进代码的想法吗?

【问题讨论】:

您能否提供PosAndDirPosDir 的定义 当然。完整代码在这里:gist.github.com/YannMoisan/18e44d8998d42d745a3ea9caaae4c16a 【参考方案1】:
    cats 将State[S, A] 定义为堆栈安全StateT[Eval, S , A] 的别名,在scalaz 术语中是StateT[Trampoline, S, A],所以runS 返回Eval[A],其中value 将在没有*** 的情况下运行,即使很长时间@ 987654332@ 个序列。

    使用更多额外的导入

    import cats.data.State, StateT
    import cats.MonadState
    import cats.syntax.functorFilter._
    import cats.instances.option._
    

    还有一些准备工作

    type Walk[x] = StateT[Option, PosAndDir, x]
    val stateMonad = MonadState[Walk, PosAndDir]
    
    import stateMonad._
    

    你可以让你的函数看起来像这样

    def update2(i: Instruction): StateT[Option, PosAndDir, Pos] =
      for (pad ← get if i == Walk) yield pad.pos
    

    不是因为这个improvement,这个解决方案在 2.12 中不起作用,你可以让它使用这个解决方法

    implicit class FunctorWithFilter[F[_] : FunctorFilter, A](fa: F[A]) 
      def withFilter(f: A ⇒ Boolean) = fa.filter(f)
    
    

    Seq 没有实例,this answer 描述了原因。虽然alleycats 项目中有一些非正统的实例。我不确定你是否需要Applicative[Seq],从你的代码中你更需要Traverse[Seq],或者如果你用sequence_甚至Foldable[Seq]替换你的sequence。 好消息是 Foldable[Iterable] 在 alleycats 中,这是我尝试为 Seq 实例定义类似的东西

    implicit val seqInstance = new MonadFilter[Seq] with Traverse[Seq] 
      def traverse[G[_] : Applicative, A, B](fa: Seq[A])(f: (A) ⇒ G[B]): G[Seq[B]] =
        fa match 
          case head +: tail ⇒ f(head).map2(traverse(tail)(f))(_ +: _)
          case _empty ⇒ Seq.empty[B].pure[G]
        
    
      def foldLeft[A, B](fa: Seq[A], b: B)(f: (B, A) ⇒ B): B = fa.foldLeft(b)(f)
    
      def foldRight[A, B](fa: Seq[A], lb: Eval[B])(f: (A, Eval[B]) ⇒ Eval[B]): Eval[B] =
        fa match 
          case head +: tail ⇒ f(head, foldRight(tail, lb)(f))
          case _empty ⇒ lb
        
    
      def pure[A](x: A): Seq[A] = Seq(x)
    
      def empty[A]: Seq[A] = Seq.empty[A]
    
      def flatMap[A, B](fa: Seq[A])(f: (A) ⇒ Seq[B]): Seq[B] = fa.flatMap(f)
    
      def tailRecM[A, B](a: A)(f: (A) ⇒ Seq[Either[A, B]]): Seq[B] = 
        @tailrec def go(seq: Seq[Either[A, B]]): Seq[B] =
          if (seq.contains((_: Either[A, B]).isLeft)) 
            go(seq.flatMap 
              case Left(a) ⇒ f(a)
              case b ⇒ Seq(b)
            ) else seq.collect  case Right(b) ⇒ b 
    
        go(Seq(Left(a)))
      
      override def mapFilter[A, B](fa: Seq[A])(f: (A) ⇒ Option[B]): Seq[B] = 
        fa.flatMap(f(_).toSeq)
    
    

    没有花太多时间,但这是我通过Monocle library 简化某些部分的尝试:

    import cats.MonadState, Foldable, Functor
    import cats.instances.option._
    import cats.syntax.foldable._
    import cats.syntax.functor._
    import cats.syntax.functorFilter._
    import monocle.macros.Lenses
    
    @Lenses
    case class Pos(x: Int, y: Int)
    
    sealed abstract class Dir(val cmd: Pos ⇒ Pos)
    
    case object South extends Dir(Pos.y.modify(_ - 1))
    case object North extends Dir(Pos.y.modify(_ + 1))
    case object East extends Dir(Pos.x.modify(_ + 1))
    case object West extends Dir(Pos.x.modify(_ - 1))
    
    @Lenses
    case class PosAndDir(pos: Pos, dir: Dir)
    
    val clockwise = Vector(North, East, South, West)
    val right: Map[Dir, Dir] = clockwise.zip(clockwise.tail :+ clockwise.head).toMap
    val left: Map[Dir, Dir] = right.map(_.swap)
    
    sealed abstract class Instruction(val cmd: PosAndDir ⇒ PosAndDir)
    case object TurnLeft extends Instruction(PosAndDir.dir.modify(left))
    case object TurnRight extends Instruction(PosAndDir.dir.modify(right))
    case object Walk extends Instruction(pd ⇒ PosAndDir.pos.modify(pd.dir.cmd)(pd))
    
    def runInstructions[F[_] : Foldable : Functor](instructions: F[Instruction])(start: PosAndDir): PosAndDir =
      instructions.map(i => State.modify(i.cmd)).sequence_.runS(start).value
    

【讨论】:

感谢您指出单片眼镜。令人惊讶的是,您将cmd 放在Directions 上,而不是分离数据和行为。为什么 ?下一个方向的预计算是聪明的。什么是 StateMonad? (我知道State 是一个单子,StateT 是一个单子转换器)。 PS:不要犹豫,我最近还有其他问题在等待如此棒的答案……

以上是关于如何使用猫和状态单子的主要内容,如果未能解决你的问题,请参考以下文章

如何对非单子使用`bound`?

场景代码题:有200个骑手都想要抢这⼀个外卖单子,如何保证只有一个骑手接到单子?

场景代码题:有200个骑手都想要抢这⼀个外卖单子,如何保证只有一个骑手接到单子?

如何将列表单子函数转换为广度优先搜索?

如何优化这个 Haskell 程序?

状态单子:从一种状态类型转换到另一种状态类型