在 <...>.sequence 方法上列出状态单子列表 "fail fast"?

Cats a List of State Monads "fail fast" on <...>.sequence method?

假设我们有一个状态列表,我们想要对它们进行排序:

import cats.data.State
import cats.instances.list._
import cats.syntax.traverse._


trait MachineState
case object ContinueRunning extends MachineState
case object StopRunning extends MachineState

case class Machine(candy: Int)

val addCandy: Int => State[Machine, MachineState] = amount =>
  State[Machine, MachineState] { machine =>
    val newCandyAmount = machine.candy + amount
    if(newCandyAmount > 10)
      (machine, StopRunning)
    else
      (machine.copy(newCandyAmount), ContinueRunning)
  }


List(addCandy(1),
     addCandy(2),
     addCandy(5),
     addCandy(10),
     addCandy(20),
     addCandy(50)).sequence.run(Machine(0)).value

结果会是

(Machine(10),List(ContinueRunning, ContinueRunning, ContinueRunning, StopRunning, StopRunning, StopRunning))

很明显,最后 3 个步骤是多余的。 有没有办法让这个序列提前停止?在这里,当返回 StopRunning 时,我想停止。 例如,Either 的列表会很快失败并在需要时提前停止序列(因为它就像一个 monad)。

记录 - 我知道可以简单地编写一个尾递归来检查正在运行的每个状态,如果满足某些条件 - 停止递归.我只想知道是否有更优雅的方法来做到这一点?递归解决方案对我来说似乎有很多样板,我错了吗?

谢谢!:))

这里有两件事需要完成。

首先是了解实际发生的事情:

  • State 获取一些状态值,许多组合调用之间的线程以及进程中的线程也会产生一些输出值
  • 在你的例子中 Machine 状态 在调用之间线程化,而 MachineState 是单个操作的输出
  • sequence(通常)采用一些参数化内容的集合(此处 Listhere State[Machine, _] 并在左侧进行嵌套(此处:List[State[Machine, _]] -> State[Machine, List[_]])(_ 是您将用您的类型填补的空白)
  • 结果是您将线程状态 (Machine(0)) 遍历所有函数,同时将每个函数的输出 (MachineState) 组合到输出列表中
// ammonite

// to better see how many times things are being run
@ {
  val addCandy: Int => State[Machine, MachineState] = amount =>
    State[Machine, MachineState] { machine =>
      val newCandyAmount = machine.candy + amount
      println("new attempt with " + machine + " and " + amount)
      if(newCandyAmount > 10)
        (machine, StopRunning)
      else
        (machine.copy(newCandyAmount), ContinueRunning)
    }
  }
addCandy: Int => State[Machine, MachineState] = ammonite.$sess.cmd24$$$Lambda69/1733815710@25c887ca

@ List(addCandy(1),
       addCandy(2),
       addCandy(5),
       addCandy(10),
       addCandy(20),
       addCandy(50)).sequence.run(Machine(0)).value
new attempt with Machine(0) and 1
new attempt with Machine(1) and 2
new attempt with Machine(3) and 5
new attempt with Machine(8) and 10
new attempt with Machine(8) and 20
new attempt with Machine(8) and 50
res25: (Machine, List[MachineState]) = (Machine(8), List(ContinueRunning, ContinueRunning, ContinueRunning, StopRunning, StopRunning, StopRunning))

换句话说,你想要的是熔断那么.sequence可能不是你想要的

事实上,您可能想要其他东西 - 将 A => (A, B) 函数列表合并到一个函数中,如果计算结果为 StopRunning(在您的code nothing 告诉代码熔断的条件是什么以及应该如何执行)。我建议用其他一些函数明确地做到这一点,例如:

@ {
  List(addCandy(1),
       addCandy(2),
       addCandy(5),
       addCandy(10),
       addCandy(20),
       addCandy(50))
    .reduce { (a, b) =>
      a.flatMap {
        // flatMap and map uses MachineState
        // - the second parameter is the result after all!
        // we are pattern matching on it to decide if we want to
        // proceed with computation or stop it

        case ContinueRunning => b // runs next computation
        case StopRunning     => State.pure(StopRunning) // returns current result without modifying it
      }
    }
    .run(Machine(0))
    .value
  }
new attempt with Machine(0) and 1
new attempt with Machine(1) and 2
new attempt with Machine(3) and 5
new attempt with Machine(8) and 10
res23: (Machine, MachineState) = (Machine(8), StopRunning)

这将消除 addCandy 中对 运行 代码的需求 - 但您无法真正摆脱将状态组合在一起的代码,因此此 reduce 逻辑将在运行时应用n-1 次(其中 n 是您列表的大小),这无济于事。

顺便说一句,如果你仔细看看 Either 你会发现它也会计算 n 结果,然后才将它们组合起来,这样看起来像是断路,但实际上不是'吨。序列正在组合 "parallel" 计算的结果,但如果其中任何一个失败,则不会中断它们。