Mik*_*len 8 scala state-monad scala-cats
我正在使用Scala Cats库中的State monad来以功能方式构成命令转换的命令式序列。
我的实际用例非常复杂,因此,为简化起见,请考虑以下最小问题:存在一种Counter状态,其计数值可能会递增或递减;但是,如果计数变为负数或溢出,则表示错误。万一遇到错误,我需要在发生错误时保留状态,并有效地停止处理后续的状态转换。
我使用type,使用每个状态转换的返回值报告任何错误Try[Unit]。成功完成的操作将返回新状态加该值Success(()),而失败将返回现有状态以及包装在其中的异常Failure。
注意:显然,遇到错误时,我可以抛出异常。但是,这将违反引用透明性,并且还需要我做一些额外的工作才能将计数器状态存储在抛出的异常中。我也打折了使用a Try[Counter]作为状态类型(而不是just Counter),因为我不能使用它来跟踪失败和失败状态。我尚未探讨的一种选择是使用(Counter, Try[Unit])元组作为状态,因为这似乎太麻烦了,但是我愿意提出建议。
import cats.data.State
import scala.util.{Failure, Success, Try}
// State being maintained: an immutable counter.
final case class Counter(count: Int)
// Type for state transition operations.
type Transition[M] = State[Counter, Try[M]]
// Operation to increment a counter.
val increment: Transition[Unit] = State {c =>
// If the count is at its maximum, incrementing it must fail.
if(c.count == Int.MaxValue) {
(c, Failure(new ArithmeticException("Attempt to overflow counter failed")))
}
// Otherwise, increment the count and indicate success.
else (c.copy(count = c.count + 1), Success(()))
}
// Operation to decrement a counter.
val decrement: Transition[Unit] = State {c =>
// If the count is zero, decrementing it must fail.
if(c.count == 0) {
(c, Failure(new ArithmeticException("Attempt to make count negative failed")))
}
// Otherwise, decrement the count and indicate success.
else (c.copy(count = c.count - 1), Success(()))
}
Run Code Online (Sandbox Code Playgroud)
但是,我正在努力确定将过渡串在一起的最佳方法,同时以所需的方式处理任何故障。(如果您愿意,可以更一般地说明我的问题,我需要根据前一个转换的返回值有条件地执行后续转换。)
例如,以下一组转换可能会在第一步,第三步或第四步失败(但让我们假设它也可能在第二步失败),具体取决于计数器的启动状态,但仍会尝试无条件执行下一步:
val counterManip: Transition[Unit] = for {
_ <- decrement
_ <- increment
_ <- increment
r <- increment
} yield r
Run Code Online (Sandbox Code Playgroud)
如果我以初始计数器值为0运行此代码,则显然我将获得的新计数器值为3和a Success(()),因为这是最后一步的结果:
scala> counterManip.run(Counter(0)).value
res0: (Counter, scala.util.Try[Unit]) = (Counter(3),Success(()))
Run Code Online (Sandbox Code Playgroud)
但是我想要的是获得初始计数器状态(decrement操作失败的状态)并ArithmeticException包装在中Failure,因为第一步失败了。
到目前为止,我唯一能提出的解决方案非常复杂,重复且容易出错:
val counterManip: Transition[Unit] = State {s0 =>
val r1 = decrement.run(s0).value
if(r1._2.isFailure) r1
else {
val r2 = increment.run(r1._1).value
if(r2._2.isFailure) r2
else {
val r3 = increment.run(r2._1).value
if(r3._2.isFailure) r3
else increment.run(r3._1).value
}
}
}
Run Code Online (Sandbox Code Playgroud)
给出正确的结果:
scala> counterMap.run(Counter(0)).value
res1: (Counter, scala.util.Try[Unit]) = (Counter(0),Failure(java.lang.ArithmeticException: Attempt to make count negative failed))
Run Code Online (Sandbox Code Playgroud)
更新资料
我想出了下面的untilFailure方法,用于运行过渡序列,直到过渡序列完成或发生错误(以先发生的为准)为止。我很喜欢它,因为它简单易用。
但是,我仍然很好奇是否有一种优雅的方法可以更直接地将转换链接在一起。(例如,如果转换只是返回的常规函数,Try[T]并且没有状态,那么我们可以使用来将调用链接在一起flatMap,从而允许构造一个for表达式,该表达式会将成功转换的结果传递给下一个转换。)
您可以提出更好的方法吗?
h!我不知道为什么我没这么早就想到。我想有时候有时候只用简单的术语来解释您的问题会迫使您重新审视它。
一种可能是处理过渡序列,以便仅在当前任务成功时才执行下一个任务。
// Run a sequence of transitions, until one fails.
def untilFailure[M](ts: List[Transition[M]]): Transition[M] = State {s =>
ts match {
// If we have an empty list, that's an error. (Cannot report a success value.)
case Nil => (s, Failure(new RuntimeException("Empty transition sequence")))
// If there's only one transition left, perform it and return the result.
case t :: Nil => t.run(s).value
// Otherwise, we have more than one transition remaining.
//
// Run the next transition. If it fails, report the failure, otherwise repeat
// for the tail.
case t :: tt => {
val r = t.run(s).value
if(r._2.isFailure) r
else untilFailure(tt).run(r._1).value
}
}
}
Run Code Online (Sandbox Code Playgroud)
然后我们可以实现counterManip为序列。
val counterManip: Transition[Unit] = for {
r <- untilFailure(List(decrement, increment, increment, increment))
} yield r
Run Code Online (Sandbox Code Playgroud)
给出正确的结果:
scala> counterManip.run(Counter(0)).value
res0: (Counter, scala.util.Try[Unit]) = (Counter(0),Failure(java.lang.ArithmeticException: Attempt to make count negative failed))
scala> counterManip.run(Counter(1)).value
res1: (Counter, scala.util.Try[Unit]) = (Counter(3),Success(()))
scala> counterManip.run(Counter(Int.MaxValue - 2)).value
res2: (Counter, scala.util.Try[Unit]) = (Counter(2147483647),Success(()))
scala> counterManip.run(Counter(Int.MaxValue - 1)).value
res3: (Counter, scala.util.Try[Unit]) = (Counter(2147483647),Failure(java.lang.ArithmeticException: Attempt to overflow counter failed))
scala> counterManip.run(Counter(Int.MaxValue)).value
res4: (Counter, scala.util.Try[Unit]) = (Counter(2147483647),Failure(java.lang.ArithmeticException: Attempt to overflow counter failed))
Run Code Online (Sandbox Code Playgroud)
缺点是所有转换都必须有一个共同的返回值(除非您对Any结果满意)。