Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should constructing stateful objects be modeled with an effect type?

When using a functional environment like Scala and cats-effect, should the construction of stateful objects be modeled with an effect type?

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

The construction isn't fallible, so we could use a weaker typeclass like Apply.

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

I guess all of these are pure and deterministic. Just not referentially transparent since the resulting instance is different each time. Is that a good time to use an effect-type? Or would there be a different functional pattern here?

like image 540
Mark Canlas Avatar asked Oct 04 '19 21:10

Mark Canlas


2 Answers

Should constructing stateful objects be modeled with an effect type?

If you are already using an effect system, it most likely has a Ref type to safely encapsulate mutable state.

So I say: model stateful objects with Ref. Since creation of (as well as access to) those is already an effect, this will automatically make creating the service effectful as well.

This neatly side-steps your original question.

If you want to manually manage internal mutable state with a regular var you have to make sure by yourself that all operations that touch this state are considered effects (and most likely also are made thread-safe), which is tedious and error-prone. This can be done, and I agree with @atl's answer that you do not strictly have to make creation of the stateful object effectful (as long as you can live with the loss of referential integrity), but why not save yourself the trouble and embrace the tools of your effect system all the way?


I guess all of these are pure and deterministic. Just not referentially transparent since the resulting instance is different each time. Is that a good time to use an effect-type?

If your question can be rephrased as

Are the additional benefits (on top of a correctly working implementation using a "weaker typeclass") of referential transparency and local reasoning enough to justify using an effect-type (which has to be already in use for state access and mutation) also for state creation ?

then: Yes, absolutely.

To give an example of why this is useful:

The following works fine, even though service creation is not made an effect:

val service = makeService(name)
for {
  _ <- service.doX()
  _ <- service.doY()
} yield Ack.Done

But if you refactor this as below you won't get a compile-time error, but you will have changed the behaviour and most likely have introduced a bug. If you had declared makeService effectful, the refactoring would not type-check and be rejected by the compiler.

for {
  _ <- makeService(name).doX()
  _ <- makeService(name).doY()
} yield Ack.Done

Granted the naming of the method as makeService (and with a parameter, too) should make it pretty clear what the method does and that the refactoring was not a safe thing to do, but "local reasoning" means that you don't have to look at naming conventions and the implementation of makeService to figure that out: Any expression that cannot be mechanically shuffled around (deduplicated, made lazy, made eager, dead-code eliminated, parallelized, delayed, cached, purged from a cache etc) without changing behaviour (i.e. is not "pure") should be typed as effectful.

like image 181
Thilo Avatar answered Nov 20 '22 16:11

Thilo


What does stateful service refer to in this case?

Do you mean that it will execute a side effect when an object is constructed? For this, a better idea would be to have a method that runs the side effect when your application is starting. Instead of running it during construction.

Or maybe you are saying that it holds a mutable state inside the service? As long as the internal mutable state is not exposed, it should be alright. You just need to provide a pure (referentially transparent) method to communicate with the service.

To expand on my second point:

Let's say we're constructing an in memory 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, this doesn't need to be effectful, since the same thing is happening if you make a network call. Although, you need to make sure that there is only one instance of this class.

If you're using Ref from cats-effect, what I would normally do, is to flatMap the ref at the entry point, so your class doesn't have to be effectful.

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, if you're writing a shared service or a library which is dependent on a stateful object (let's say multiple concurrency primitives) and you don't want your users to care what to initialize.

Then, yes, it has to be wrapped in an effect. You could use something like, Resource[F, MyStatefulService] to make sure that everything is closed properly. Or just F[MyStatefulService] if there's nothing to close.

like image 24
atl Avatar answered Nov 20 '22 14:11

atl