Scala状态单子 - 组合不同的状态类型

15

我正在学习状态单子(State monad)。简单的例子很容易理解。现在我要转向一个现实世界的案例,其中领域对象是复合的。例如,对于以下领域对象(它们没有太多意义,只是一个纯粹的例子):

case class Master(workers: Map[String, Worker])
case class Worker(elapsed: Long, result: Vector[String])
case class Message(workerId: String, work: String, elapsed: Long)

Worker视为State[S,+A]单子中的S类型,编写一些组合器非常容易,例如:

type WorkerState[+A] = State[Worker, A]
def update(message: Message): WorkerState[Unit] = State.modify { w =>
    w.copy(elapsed = w.elapsed + message.elapsed,
           result = w.result :+ message.work)
}
def getWork: WorkerState[Vector[String]] = State { w => (w.result, w) }
def getElapsed: WorkerState[Long] = State { w => (w.elapsed, w) }
def updateAndGetElapsed(message: Message): WorkerState[Long] = for {
    _ <- update(message)
    elapsed <- getElapsed
} yield elapsed
// etc.

什么是将这些组合与Master状态组合器结合的惯用方式?例如:
type MasterState[+A] = State[Master, A]
def updateAndGetElapsedTime(message: Message): MasterState[Option[Long]]

我可以这样实现:
def updateAndGetElapsedTime(message: Message): MasterState[Option[Long]] =   
    State { m =>
        m.workers.get(message.workerId) match {
            case None => (None, m)
            case Some(w) =>
                val (t, newW) = updateAndGetElapsed(message).run(w)
                (Some(t), m.copy(m.workers.updated(message.workerId, newW))
        }
    }

我不喜欢的是我必须在最后一个转换器内手动运行State Monad。我的实际例子更加复杂,采用这种方法很快就会变得混乱。

有没有更符合惯用法的方法来运行这种增量更新呢?


不错的问题!您是在提到像scalaz这样的具体State实现吗? - Odomontois
这绝对是LensT使用的好例子,迫不及待想看到一些专家的回答。 - Odomontois
1个回答

8

通过结合镜头和状态单子,可以很好地完成这个任务。首先进行设置(我已经轻微编辑了您的设置,使其与Scalaz 7.1兼容):

case class Master(workers: Map[String, Worker])
case class Worker(elapsed: Long, result: Vector[String])
case class Message(workerId: String, work: String, elapsed: Long)

import scalaz._, Scalaz._

type WorkerState[A] = State[Worker, A]

def update(message: Message): WorkerState[Unit] = State.modify { w =>
  w.copy(
    elapsed = w.elapsed + message.elapsed,
    result = w.result :+ message.work
  )
}

def getWork: WorkerState[Vector[String]] = State.gets(_.result)
def getElapsed: WorkerState[Long] = State.gets(_.elapsed)
def updateAndGetElapsed(message: Message): WorkerState[Long] = for {
  _ <- update(message)
  elapsed <- getElapsed
} yield elapsed

现在我们来介绍一下几个通用镜头,它们可以让我们查看Master内部:

val workersLens: Lens[Master, Map[String, Worker]] = Lens.lensu(
  (m, ws) => m.copy(workers = ws),
  _.workers
)

def workerLens(workerId: String): PLens[Master, Worker] =
  workersLens.partial andThen PLens.mapVPLens(workerId)

然后我们基本上就完成了:
def updateAndGetElapsedTime(message: Message): State[Master, Option[Long]] =
  workerLens(message.workerId) %%= updateAndGetElapsed(message)

这里的%%=告诉我们,一旦通过透镜放大到适当的工作人员,要执行什么状态操作。

我正在运行自己的“穷人单子”实现。我理解得正确吗?为了使其工作,我需要一个Lens的实现,并将State与Lens集成(特别是那个%%=操作)? - ak.
另外,您对使用Scalaz与手写Monad实现有何看法? - ak.
如果你正在做这种事情,我强烈建议使用Scalaz或cats而不是自己编写。 - Travis Brown
@ak 我看到你自己的 Statescalaz.State 的主要区别在于 runState 结果元组中的顺序。我猜你至少可以通过隐式转换将这些实现粘合在一起。 - Odomontois
这似乎是一个解决常见问题的好方法。它还可以很好地处理具有不同类型和自己状态的多个组件。我想知道是否有任何类型级别的库已经采用了State + Lens的组合?也许还会有一个类型类来帮助转发对组件的调用。 - James McCabe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接