什么是管理可变状态的最佳方式?

5

我刚刚完成了Coursera上Martin Odersky的scala课程。作为我的第一门函数式编程语言,我对限制可变状态的想法感到兴奋。这样可以更容易地进行并发操作,同时也使代码更加易于维护。

在学习过程中,我意识到只要一个对象没有可变变量且仅引用不可变对象,就可以保证其不可变性。因此,现在我可以通过创建新状态来完成所有操作,而无需修改旧状态,并尽可能使用尾递归。

很好。所以我只能在一定程度上做到这一点。在某些时候,我的应用程序需要能够修改一些现有状态。我知道在这一点上要放置并发控制,锁等等。但我仍然默认使用我一直使用的标准多线程并发控制。

哦,scala社区,有更好的方法吗?也许是单子?

编辑:这个问题有点普遍化,所以我想举一个用例: 我有一个机器学习算法,存储着几个数据集合。它们有返回更新后数据表示的函数(如训练等),全部是不可变的。最终,我可以将这种返回-更新状态的模式延续到运行模拟的实际对象。这个对象持有对集合的引用,因此它具有可变状态。我可能希望将其分布到多个核心或多个系统中。


4
在演员内部隐藏可变性。 - om-nom-nom
我之前在一个项目中使用过Akka Actors,它非常出色地完成了任务,并且在服务器负载分配方面表现良好。是否有适用于本地规模的更轻量级的Actor版本? - Colin Godsey
1
我认为这个问题过于主观。或许更适合发表在programmers.stackexchange上。 - frankc
@ColinGodsey 还有 Scala Actors(与 Akka Actors 相对应的实现),它是标准 Scala 发行版附带的。它们预计将来会被 Akka actors 取代,但你试过它们吗? - Faiz
2
@ColinGodsey:我也在使用Scala进行机器学习,我对不可变状态的更新性能存在一个相关的疑虑。我们都知道不可变性的美妙之处,但在我看来,机器学习是可变数据结构有时更为直接易用和/或更快的完美示例,所以要谨慎选择。 - bluenote10
@bluenote10 这恰好描述了我的情况,并确认了我怀疑的一些事情——最好明智地使用可变状态。 - Colin Godsey
3个回答

4
这是一个比较主观的问题,所以我不会试图回答其中“哪个最好”的部分。如果你最关心的是在多线程并发的情况下状态(state)的问题,那么一种选择可能是软件事务内存

有一个实现(参见快速入门)Akka提供的STM。根据您的用例,它可能会很重或过度,但它可能比一堆锁更可取。与锁不同,STM倾向于是乐观的,就像数据库事务一样。与数据库事务一样,在事务上下文中显式地对共享状态进行更改,并且如果检测到冲突,则会原子地提交或重新尝试描述的更改。基本上,您必须将所有状态包装在Refs中,这些Refs只能在“原子”块中操作-作为接受闭包的方法实现,其中您使用操作Refs并且ScalaSTM确保对状态的整个操作集成功或失败-不会有不完全或不一致的更改。

这是利用Scala的"implicit parameters"——所有针对“Ref”的操作都需要一个“transaction”对象作为参数,并且在给定给“atomic”的闭包中可以声明为implicit,因此在“atomic”内部的所有代码都可以以非常自然而又安全的方式编写。
问题是,为了使其有用,您确实需要使用提供的事务数据结构;这意味着在事务上下文(在原子块内)中使用“TSet”而不是“Set”,“TMap”而不是“Map”。这些在原子上下文中提供了全有或全无的更新语义。它们非常类似于Clojure的persistent collections。您还可以使用“Ref”构建自己的事务数据结构,以在这些“atomic”块中使用。
如果您不反对括号,则Clojure关于引用的解释非常好:http://clojure.org/refs

这里是对STM的概念进行解释,并附带Scala示例的文章。http://ppl.stanford.edu/papers/scaladays2010bronson.pdf - idonnie
STM听起来真的很棒,我想那可能是我想要坚持使用的模型。这些模式也直接适用于与数据网格或数据库的使用,因此它非常适合实现稍后可能分布的系统。 - Colin Godsey
如果您计划实施一个可能会在以后分布的系统,那么仅使用STM可能不太有帮助 - STM通常用于协调单个进程(单个JVM)内共享状态的访问。全面了解Akka可能是一个不错的选择,特别是其事务处理功能。 - Faiz

2
根据您的用例,您可能能够坚持使用深度不可变对象结构,部分复制它们而不是实际地改变它们(类似于“更新”不可变列表与其原始列表共享后缀)。所谓的“镜头”是处理这种结构的好方法,请在此SO问题此博客文章中阅读有关它们的内容。
当然,坚持使用不可变结构只适用于您不希望更改在全局范围内可观察到的情况。一个最有可能不是选择不可变结构的例子是两个并发客户端正在共享列表的情况,其中客户端A进行的修改必须由客户端B观察到,反之亦然。

我通常尝试坚持使用深度不可变的结构,镜头听起来很有趣...我需要做更多的阅读,我不确定我是否真正掌握了这个概念。 - Colin Godsey

0

我建议最好的方法是将可变变量存储在Akka actor内部,使用消息传递进出Akka actor来发送和接收此可变引用。使用不可变数据结构。

我有一个StorageActor如下所示。每次通过StoreEntity存储东西时,变量entityMap都会更新。而且它不需要是易失性的,仍然可以工作。

Akka actor是可以改变事物的地方,消息在纯函数世界中进出。

import akka.actor.Actor
import java.util.UUID
import com.orsa.minutesheet.entity.Entity

case class EntityRef(entity: Option[Entity])

case class FindEntity(uuid: UUID)
case class StoreEntity[T >: Entity](uuid: UUID, entity: Option[T])

class StorageActor extends Actor {

  private var entityMap = Map[UUID, Entity]()

  private def findEntityByUUID(uuid:UUID): Option[Entity] = entityMap.get(uuid)

  def receive = {
    case FindEntity(uuid) => sender ! EntityRef( findEntityByUUID(uuid) )
    case StoreEntity(uuid, entity) =>
      entity match {
        case Some(store) => entityMap += uuid -> store.asInstanceOf[Entity]
        case None => entityMap -= uuid
      }
  }
}

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