当使用像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]我猜所有这些都是纯粹的和确定性的。只是引用不透明,因为每次生成的实例都是不同的。这是使用效果类型的好时机吗?或者这里会有不同的功能模式?
发布于 2019-11-16 21:38:22
构造有状态对象是否应该使用效果类型建模?
如果您已经在使用一个effect系统,那么它很可能有一个Ref类型来安全地封装可变状态。
所以我说:用Ref. 对有状态对象进行建模由于这些服务的创建(以及对它们的访问)已经生效,这将自动使创建服务变得有效。
这巧妙地回避了你最初的问题。
如果您希望使用常规var手动管理内部可变状态,则必须自己确保所有接触此状态的操作都被视为效果(并且很可能也是线程安全的),这是繁琐且容易出错的。这是可以做到的,我同意@atl的回答,即您不需要严格地使有状态对象的创建有效(只要您可以忍受引用完整性的损失),但是为什么不省去您自己的麻烦,一直使用您的effect系统的工具呢?
我猜所有这些都是纯粹的和确定性的。只是引用不透明,因为每次生成的实例都是不同的。这是使用效果类型的好时机吗?
如果你的问题可以重新表述为
引用透明性和局部推理的额外好处(在使用“较弱类型类”的正确工作的实现之上)是否足以证明使用效果类型(必须已经用于状态访问和突变)也适用于状态创建?
然后:是的,完全正确。
举个例子说明为什么这样做很有用:
下面的代码运行良好,即使创建服务不会产生影响:
val service = makeService(name)
for {
_ <- service.doX()
_ <- service.doY()
} yield Ack.Done但是如果你像下面这样重构它,你不会得到一个编译时错误,但是你会改变行为,并且很可能引入了一个bug。如果您已经声明了makeService有效,那么重构将不会进行类型检查,并且会被编译器拒绝。
for {
_ <- makeService(name).doX()
_ <- makeService(name).doY()
} yield Ack.Done假设将方法命名为makeService (也有一个参数),应该清楚地表明该方法做了什么,重构不是一件安全的事情,但“本地推理”意味着你不必查看命名约定和makeService的实现来弄清楚这一点:任何不能在不改变行为(即不是“纯”的)的情况下机械地混洗(去重、变懒、变得急切、消除死代码、并行化、延迟、缓存、从缓存中清除)的表达式都应该被定义为有效的类型。
发布于 2019-11-16 15:34:59
在这种情况下,有状态服务指的是什么?
你的意思是,当一个对象被构造时,它会执行副作用?为此,一个更好的想法是有一个在应用程序启动时运行副作用的方法。而不是在构造过程中运行它。
或者你可能是说它在服务中拥有一个可变的状态?只要不暴露内部可变状态,就应该没问题。您只需要提供一个纯(引用透明的)方法来与服务通信。
对我的第二点进行扩展:
假设我们正在构造一个内存中的db。
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,我通常会做的是在入口点flatMap这个ref,这样你的类就不需要有效了。
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]。
https://stackoverflow.com/questions/58243347
复制相似问题