在使用诸如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]
我想所有这些都是纯粹的和确定性的。只是不是参照透明的,因为每次生成的实例都是不同的。那是使用效果类型的好时机吗?还是这里会有不同的功能模式?
答案 0 :(得分:3)
构造状态对象应该用效果类型建模吗?
如果您已经在使用效果系统,则它很可能具有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
的实现就可以发现:不能机械地拖拉的任何表达式(去重复,变得懒惰,渴望,死代码消除,并行化,延迟,缓存,从缓存中清除等),而无需更改行为(即不是“纯”),则应输入有效。
答案 1 :(得分:2)
在这种情况下,有状态服务指的是什么?
您是说它在构造对象时会产生副作用吗? 为此,一个更好的主意是拥有一种在应用程序启动时运行副作用的方法。而不是在构建过程中运行它。
或者也许您是说它在服务内部拥有可变状态?只要不暴露内部可变状态,就可以了。您只需要提供一种纯(相对透明)的方法即可与服务进行通信。
要扩展我的第二点:
假设我们正在构建一个内存数据库。
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
引用,因此您的类不必是有效的。 / p>
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]
。