首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >构造有状态对象是否应该使用effect类型建模?

构造有状态对象是否应该使用effect类型建模?
EN

Stack Overflow用户
提问于 2019-10-05 05:26:48
回答 2查看 349关注 0票数 9

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

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// never throws
def withWeakEffect[F: Applicative](name: String): F[Service] =
  new Service(name).pure[F]

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

EN

回答 2

Stack Overflow用户

发布于 2019-11-16 21:38:22

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

如果您已经在使用一个effect系统,那么它很可能有一个Ref类型来安全地封装可变状态。

所以我说:用Ref. 对有状态对象进行建模由于这些服务的创建(以及对它们的访问)已经生效,这将自动使创建服务变得有效。

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

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

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

如果你的问题可以重新表述为

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

然后:是的,完全正确。

举个例子说明为什么这样做很有用:

下面的代码运行良好,即使创建服务不会产生影响:

代码语言:javascript
复制
val service = makeService(name)
for {
  _ <- service.doX()
  _ <- service.doY()
} yield Ack.Done

但是如果你像下面这样重构它,你不会得到一个编译时错误,但是你会改变行为,并且很可能引入了一个bug。如果您已经声明了makeService有效,那么重构将不会进行类型检查,并且会被编译器拒绝。

代码语言:javascript
复制
for {
  _ <- makeService(name).doX()
  _ <- makeService(name).doY()
} yield Ack.Done

假设将方法命名为makeService (也有一个参数),应该清楚地表明该方法做了什么,重构不是一件安全的事情,但“本地推理”意味着你不必查看命名约定和makeService的实现来弄清楚这一点:任何不能在不改变行为(即不是“纯”的)的情况下机械地混洗(去重、变懒、变得急切、消除死代码、并行化、延迟、缓存、从缓存中清除)的表达式都应该被定义为有效的类型。

票数 3
EN

Stack Overflow用户

发布于 2019-11-16 15:34:59

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

你的意思是,当一个对象被构造时,它会执行副作用?为此,一个更好的想法是有一个在应用程序启动时运行副作用的方法。而不是在构造过程中运行它。

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

对我的第二点进行扩展:

假设我们正在构造一个内存中的db。

代码语言:javascript
复制
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,这样你的类就不需要有效了。

代码语言:javascript
复制
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]

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/58243347

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档