9

在使用 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]

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

4

2 回答 2

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找出:任何不能在不改变行为( ie is not "pure") 应输入为有效。

于 2019-11-16T13:38:22.430 回答
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,这不需要有效,因为如果您进行网络调用,也会发生同样的事情。虽然,您需要确保该类只有一个实例。

如果你使用Reffrom cats-effect,我通常会做的是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]没有什么可以关闭的。

于 2019-11-16T07:34:59.153 回答