如何在State满足条件时终止Scalaz/Cats中的状态转换?
嘿,作为Scalaz/Cats新手遇到这个问题很正常——其实咱们完全可以用State monad本身的特性实现这种「条件式停止」的逻辑,根本不需要额外工具!
你之前的代码里,transition是硬编码了三次pop操作,不管栈里还有没有元素都会执行到底。而要实现「直到栈为空就停止pop」的需求,核心是利用State的flatMap能力:它允许我们根据当前的状态和上一步的执行结果来动态决定下一步要执行的State操作。
用Cats实现的示例
先导入Cats的相关工具:
import cats.data.State import cats.implicits._
定义栈类型和基础的pop操作(和你原来的逻辑一致):
type Stack = List[Int] def pop: State[Stack, Option[Int]] = State { case x :: xs => (xs, Some(x)) case Nil => (Nil, None) }
接下来写核心的popUntilEmpty函数,它会不断执行pop直到栈为空,同时收集所有弹出的元素:
def popUntilEmpty: State[Stack, List[Int]] = for { // 先检查当前栈是否为空 isEmpty <- State.inspect[Stack, Boolean](_.isEmpty) // 根据检查结果决定后续操作 result <- if (isEmpty) { // 栈空了,直接返回空列表,状态保持不变 State.pure(Nil) } else { // 先弹出一个元素,再递归执行剩余的pop操作,最后把结果拼接起来 for { popped <- pop rest <- popUntilEmpty } yield popped.get :: rest // 因为isEmpty为false,这里popped肯定是Some值 } } yield result
如果你的需求是弹出元素后,一旦栈变为空就停止并返回当前弹出的元素(而不是收集所有元素),可以调整成这样:
def popUntilStackIsEmpty: State[Stack, Option[Int]] = for { currentStack <- State.get[Stack] result <- if (currentStack.isEmpty) { State.pure(None) } else { for { popped <- pop newStack <- State.get[Stack] finalResult <- if (newStack.isEmpty) { // 弹出后栈空了,返回当前弹出的元素 State.pure(popped) } else { // 栈还没空,继续递归执行 popUntilStackIsEmpty } } yield finalResult } } yield result
测试一下效果
运行代码看看结果:
// 测试popUntilEmpty println(popUntilEmpty.run(List(1, 2, 3, 4)).value) // 输出 (Nil, List(1, 2, 3, 4)) println(popUntilEmpty.run(Nil).value) // 输出 (Nil, Nil) // 测试popUntilStackIsEmpty println(popUntilStackIsEmpty.run(List(1, 2, 3, 4)).value) // 输出 (Nil, Some(4)) println(popUntilStackIsEmpty.run(List(1, 2)).value) // 输出 (Nil, Some(2))
原理说明
State monad的flatMap是实现这种动态逻辑的关键:每一步操作都可以先获取当前的状态(用State.get或State.inspect),然后根据状态决定是继续执行还是终止。通过递归调用State操作,我们就实现了循环式的逻辑,完全不需要跳出State的上下文。
如果用Scalaz的话,写法几乎一致,只是把导入换成import scalaz.State,State.inspect对应Scalaz的State.gets,State.pure对应State.state,核心逻辑完全相同。
内容的提问来源于stack exchange,提问作者St.Antario
相关产品推荐
相关产品推荐

