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


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)
  }

构造不是容易犯错的,因此我们可以使用较弱的typeclass,例如Apply

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

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


2
是的,创建可变状态是一个副作用。这样,它应该在a内部发生delay并返回F [Service]。例如,请参见IOstart上的方法,它返回IO [Fiber [IO,?]]而不是普通光纤。
Luis MiguelMejíaSuárez,

1
有关此问题的完整答案,请参见thisthis
Luis MiguelMejíaSuárez,

Answers:


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要弄清楚:在不更改行为的情况下,任何不能机械地乱序处理(去重复,变得懒惰,变得急切,死代码消除,并行化,延迟,缓存,从缓存中清除等)的表达式。即不是“纯”)应输入为有效。


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,这不一定是有效的,因为如果您进行网络呼叫,也会发生同样的事情。虽然,您需要确保该类只有一个实例。

如果您使用的Ref是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]没有什么可关闭的。


“您只需要提供一种与服务进行通信的纯方法”,或者可能恰恰相反:纯粹内部状态的初始构造并不一定是一种效果,但是在服务上与该可变状态交互的任何操作然后需要将任何方式标记为有效(以避免发生类似的事故val neverRunningThisButStillMessingUpState = Task.pure(service.changeStateThinkingThisIsPure()).repeat(5)
Thilo

或来自另一端:是否使服务创建有效或无效并不重要。但是,无论您采用哪种方式,以任何方式与该服务进行交互都必须有效(因为它内部携带的可变状态会受到这些交互的影响)。
Thilo

1
@thilo是的,您是对的。我的意思pure是它必须是参照透明的。例如考虑未来的例子。val x = Future {... }def x = Future { ... }意味着不同的事情。(在重构代码时,这可能会咬住您)但是,cats-effect,monix或zio并非如此。
atl
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.