What is the benefit of effect system (e.g. ZIO)?
Asked Answered
M

4

7

I'm having hard time understanding what value effect systems, like ZIO or Cats Effect.

  • It does not make code readable, e.g.:
val wrappedB = for {
   a <- getA()  // : ZIO[R, E, A]
   b <- getB(a) // : ZIO[R, E, B]
} yield b

is no more readable to me than:

val a = getA()  // : A
val b = getB(a) // : B

I could even argue, that the latter is more straight forward, because calling a function executes it, instead of just creating an effect or execution pipeline.

  • Delayed execution does not sound convincing, because all examples I've encountered so far are just executing the pipeline right away anyways. Being able to execute effects in parallel or multiple time can be achieved in simpler ways IMHO, e.g. C# has Parallel.ForEach
  • Composability. Functions can be composed without using effects, e.g. by plain composition.
  • Pure functional methods. In the end the pure instructions will be executed, so it seems like it's just pretending DB access is pure. It does not help to reason, because while construction of the instructions is pure, executing them is not.

I may be missing something or just downplaying the benefits above or maybe benefits are bigger in certain situations (e.g. complex domain). What are the biggest selling points to use effect systems?

Morganatic answered 20/12, 2021 at 20:39 Comment(1)
Check the "Programs as Values" series from Fabio here: systemfw.org/archive.html and then if you have follow up questions / criticism then I would recommend you to move it into either the Scala discord server or the Typelevel one since, IMHO, this topic is easier to elaborate in a conversation than in a post.Molality
U
1

Because it makes it easy to deal with side effects. From your example:

a <- getA()  // ZIO[R, E, A] (doesn't have to be ZIO btw)

val a = getA(): A

The first getA accounts in the effect and the possibility of returning an error, a side effect. This would be like getting an A from some db where the said A may not exist or that you lack permission to access it. The second getA would be like a simple def getA = "A".
How do we put these methods together ? What if one throws an error ? Should we still proceed to the next method or just quit it ? What if one blocks your thread ?

Hopefully that addresses your second point about composability. To quickly address the rest:

  • Delayed execution. There are probably two reasons for this. The first is you actually don't want to accidentally start an execution. Or just because you write it it starts right away. This breaks what the cool guys refer to as referential transparency. The second is concurrent execution requires a thread pool or execution context. Normally we want to have a centralized place where we can fine tune it for the whole app. And when building a library we can't provide it ourselves. It's the users who provide it. In fact we can also defer the effect. All you do is define how the effect should behave and the users can use ZIO, Monix, etc, it's totally up to them.
  • Purity. Technically speaking wrapping a process in a pure effect doesn't necessarily mean the underlying process actually uses it. Only the implementation knows if it's really used or not. What we can do is lift it to make it compatible with the composition.
Utopian answered 21/12, 2021 at 3:59 Comment(0)
N
1

The two examples are not comparable since an error in the first statement will mark as faulty the value equal to the objectified sequence in the first form while it will halt the whole program in the second. The second form shall then be a function definition to properly encapsulate the two statements, followed by an affectation of the result of its call.

But more than that, in order to completely mimic the first form, some additional code has to be written, to catch exceptions and build a true faulty result, while all these things are made for free by ZIO...

I think that the ability to cleanly propagate the error state between successive statements is the real value of the ZIO approach. Any composite ZIO program fragment is then fully composable itself. That's the main benefit of any workflow based approach, anyway.

It is this modularity which gives to effect handling its real value. Since an effect is an action which structurally may produce errors, handling effects like this is an excellent way to handle errors in a composable way. In fact, handling effects consists in handling errors !

Nammu answered 15/2, 2022 at 16:50 Comment(0)
A
1

what makes programming with ZIO or Cats great is when it comes to concurrent programming. They are also other reasons but this one is IMHO where I got the "Ah Ah! Now I got it".

Try to write a program that monitor the content of several folders and for each files added to the folders parse their content but not more than 4 files at the same time. (Like the example in the video "What Java developpers could learn from ZIO" By Adam Fraser on youtube https://www.youtube.com/watch?v=wxpkMojvz24 .

I mean this in ZIO is really easy to write :)

The all idea behind the fact that you combine data structure (A ZIO is a data structure) in order to make bigger data structure is so easy to understand that I would not want to code without it for complex problems :)

Aerate answered 21/2, 2022 at 16:59 Comment(0)
H
0

Yes, in a simple use case it's an overkill. Now imagine if function getA() could throw exception and function getB(a) calls 3-rd party service with expensive I/O operation inside and returns nullable. So the code would look something like this in comparison:

def getAOOP: A
def getBOOP: B

def getAEffect: Either[Exception, A] 
def getBEffect: IO[Option[B]]

Now let's re-consider mentioned benefits:

  • Delayed execution - without effects you're code suddenly turns into a minefield because your functions can randomly throw and block execution threads (which could be disastrous if you run things on execution pool without handling such cases explicitly). And with effects it's much safer and easier to configure execution pools as you like without having pools starvation/exhaustion surprises in runtime.

  • Composability. for similar reasons it's much easier to compose now. Because you know exactly what are those functions doing and you have instruments like Traversable/Sequencable to facilitate composition here. You may argue that having 3 nested types is hard, but they could be folded one into another for simplicity very nicely on the higher levels (so insted of IO[Either[Throwable, B]] you could have just IO[B] because IO has error channel already.

  • Pure functional methods. Having them pure opens up a lot of new possibilities, I already mentioned that having IO as a description of I/O operation enables flexibility and convenience in configuring thread pools and execution patterns. Bat the same true, for example, for DB operations. Having them as a pure descriptions enables you to extract connectivity logic to a separate place, so for example you could have DBIO describing what needs to be run and Transactor describing how it needs to be run, etc. Which in turn enables you to write much more maintainable code.

And in addition: it's easier to change, easier to reason about the code in isolation, and last but not least it integrates well with Effect ecosystem like Cats.effect or Zio.

Hem answered 24/11, 2023 at 13:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.