构造有状态对象是否应使用效果类型进行建模?



当使用像Scala和cats-effect这样的功能环境时,有状态对象的构造是否应该使用效果类型进行建模?

// not a value/case class
class Service(s: name)
def withoutEffect(name: String): Service =
new Service(name)
def withEffect[F: Sync](name: String): F[Service] =
F.delay {
new Service(name)
}

构造不会出错,因此我们可以使用较弱的类型类,例如Apply

// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
new Service(name).pure[F]

我想所有这些都是纯粹的和确定性的。只是引用不透明,因为每次生成的实例都不同。这是使用效果类型的好时机吗?还是这里有不同的功能模式?

构造有状态对象是否应该使用效果类型进行建模?

如果您已经在使用效果系统,则它很可能具有Ref类型来安全地封装可变状态。

所以我说:Ref对有状态对象进行建模。由于创建(以及访问(这些已经是一种效果,这将自动使创建服务也有效。

这巧妙地回避了您最初的问题。

如果要使用常规var手动管理内部可变状态,则必须自己确保所有接触此状态的操作都被视为效果(并且很可能也是线程安全的(,这很乏味且容易出错。这是可以做到的,我同意@atl的回答,即您不必严格地使有状态对象的创建有效(只要您可以忍受引用完整性的丧失(,但为什么不省去麻烦并一直拥抱效果系统的工具呢?


我想所有这些都是纯粹的和确定性的。只是引用不透明,因为每次生成的实例都不同。这是使用效果类型的好时机吗?

如果您的问题可以改写为

引用透明度和局部推理的额外好处(在使用"较弱类型类"的正确工作实现之上(是否足以证明使用效果类型(必须已经用于状态访问和突变(也用于状态创建?

然后:是的,绝对是

举例说明为什么这很有用:

即使服务创建不起作用,以下方法也可以正常工作:

val service = makeService(name)
for {
_ <- service.doX()
_ <- service.doY()
} yield Ack.Done

但是,如果您按如下所示重构它,则不会收到编译时错误,但您将更改行为并且很可能引入了错误。如果已声明makeService有效,则重构不会进行类型检查,并且会被编译器拒绝。

for {
_ <- makeService(name).doX()
_ <- makeService(name).doY()
} yield Ack.Done

给定方法的命名为makeService(以及参数(应该非常清楚该方法的作用,并且重构不是一件安全的事情,但是"本地推理"意味着您不必查看命名约定和makeService的实现来弄清楚: 任何不能机械地打乱的表达式(重复数据删除、惰性、急切、消除死代码、并行化、延迟、缓存、从缓存中清除等(而不改变行为(即不是"纯"(都应该被键入为有效。

在这种情况下,有状态服务指的是什么?

您的意思是在构造对象时执行副作用吗?为此,更好的主意是使用一种在应用程序启动时运行副作用的方法。而不是在施工期间运行它。

或者,也许您是说它在服务中具有可变状态?只要内部可变状态不暴露,应该没问题。您只需要提供一个纯(引用透明(方法来与服务通信。

扩展我的第二点:

假设我们正在构造一个内存数据库。

class InMemoryDB(private val hashMap: ConcurrentHashMap[String, String]) {
def getId(s: String): IO[String] = ???
def setId(s: String): IO[Unit] = ???
}
object InMemoryDB {
def apply(hashMap: ConcurrentHashMap[String, String]) = new InMemoryDB(hashMap)
}

IMO,这不需要有效,因为同样的事情正在发生 如果您进行网络呼叫。虽然,您需要确保此类只有一个实例。

如果你使用来自 cats-effect 的Ref,我通常会做的是在入口点flatMapref,这样你的类就不必有效。

object Effectful extends IOApp {
class InMemoryDB(storage: Ref[IO, Map[String, String]]) {
def getId(s: String): IO[String] = ???
def setId(s: String): IO[Unit] = ???
}
override def run(args: List[String]): IO[ExitCode] = {
for {
storage <- Ref.of[IO, Map[String, String]](Map.empty[String, String])
_ = app(storage)
} yield ExitCode.Success
}
def app(storage: Ref[IO, Map[String, String]]): InMemoryDB = {
new InMemoryDB(storage)
}
}

OTOH,如果您正在编写依赖于有状态对象(假设多个并发原语(的共享服务或库,并且您不希望您的用户关心要初始化的内容。

然后,是的,它必须被包裹在一个效果中。您可以使用类似Resource[F, MyStatefulService]的东西来确保所有内容都正确关闭。或者只是F[MyStatefulService],如果没有什么可关闭的。

最新更新