zio / zio-prelude

A lightweight, distinctly Scala take on functional abstractions, with tight ZIO integration
https://zio.dev/zio-prelude
Apache License 2.0
447 stars 115 forks source link

Remove dependency on ZIO #213

Open rnd4222 opened 4 years ago

rnd4222 commented 4 years ago

It is important that zio would depend on zio-prelude, not the other way around, for the following reason:

  1. Loss of Abstraction. Any abstraction that features a concrete instance of that abstraction inside the abstraction (e.g. ZIO appears in the zio-prelude typeclasses) is no longer an abstraction at all. To define the meaning of an effect in terms of a concrete data type is to abandon the conception of abstraction. There is no longer abstraction, just layers of ceremony around pretend abstraction.

  2. Massive Wasted Effort. For example, cats-effect guys regularly port features back and forth between cats-effect IO and monix Task. The tiny functional programming ecosystem is maintaining essentially two copies of the same data type, with the caveat that monix Task can be regarded as a much better version of cats-effect IO, supporting everything that cats-effect IO does, in a more flexible way, and with more features.

  3. Harmful Biases. There is non-zero possibility that some 3rd-party library would get cool feature that is not easy to incorporate into ZIO. Much of the pushback would came from the fact that any significant design decision must be accompanied by significant re-engineering of the ZIO data type, resulting in decisions that are biased toward the current design, even if it's suboptimal in the landscape of possible designs. In the best possible world, zio-prelude provides a layer of abstraction which is influenced only by the libraries that consume and provide instances.

  4. Crippling Innovation. There is repeated significant demand to add more features to the ZIO datatype - features that may be present in other libraries like cats-effect IO and monix Task. Yet adding an endless battery of never-ending features is not good for this library, because it increases maintenance burden and spirals the project away from its initial goals. If ZIO were a separate project, then maintainers of other projects could feel free to contribute improvements so long as they were committed to maintaining them; and likely they would diverge from ZIO and evolve in their own ways.

  5. Zero Benefit. Having a dependency on zio in zio-prelude has zero benefit and all cost. In a world in which there is no such dependency, and the user prefers ZIO for some reason, this user can switch their SBT build file from zio-prelude to just zio and continue to use everything almost exactly as they do today. In fact, such a project can easily add extension methods so the user still has toZIO and other features they might like to use.

In summary, because having a concrete data type in the hierarchy destroys abstraction, results in massive wasted effort, biases design discussions towards a "reference type", cripples innovation, and has zero benefit, removing dependency on zio makes a ton of sense.

Then the original design goal of this project to provide a lightweight way to abstract over the different effect data types can be restored, in a healthier way that will lead to better abstractions and possibly even an improved ZIO.

adamgfraser commented 4 years ago

@rnd4222 Thanks for raising! This is definitely something we have thought about it since it is the approach that other functional programming libraries have taken. However, I don't think it is the right approach for us.

First, I don't think that it makes sense for ZIO to depend on ZIO Prelude. ZIO is a library that is focused on solving problems in asynchronous and concurrent programming using the power of pure functional programming. Some users of ZIO are obviously very familiar with functional programming abstractions but some users have no knowledge or interest in them. So making ZIO depend on ZIO Prelude would inevitably lead to these abstractions leaking into the ZIO API in a way that would go against ZIO's goals of zero dependencies and of using functional programming under the hood to solve concrete problems without users having to learn about things like type classes.

Second, I think it does make sense for ZIO Prelude to depend on ZIO. The extent to which ZIO Prelude depends on ZIO is limited to a few relatively specific areas: (1) providing instances for ZIO data types, (2) using ZIO Test to describe to laws as values, and (3) using ZIO Test assertions to support composable validation. If you look at a type class like Equal or Traversable ZIO appears nowhere in the definition of those abstractions. So I don't think that tight integration with ZIO results in a loss of abstraction and it provides concrete benefits.

More broadly, I think the goals of this library are different than you suggest. ZIO Prelude is focused on providing fundamental functional programming abstractions (e.g. associative ways of combining data, ways of traversing data structures) rather than ways of abstracting over effect types. There are no type classes describing concepts like time or resources that you would find in such an effort. There are other libraries like Izumi BIO that support abstracting over effect types in a modern way for users who are interested in that. So I don't think the other concerns you raise are applicable.

Hope this helps!

joroKr21 commented 4 years ago

There is a third option - neither depends on the other, instances live in their own module.

adamgfraser commented 4 years ago

That is certainly a possibility. I think the practical implications are:

  1. Users need to a do a third import to get all the instances.
  2. Smart type functionality out of the box is significantly degraded. Instead of users being able to use assertions with out of the box composability and error reporting they would need to provide their own predicates and error messages. A more robust version could be provided in this interop module. Similarly Validation's apply method would have to go away and be provided in this third library as some kind of implicit syntax or alternative object.
  3. Laws go away from this project and into a separate project that users also have to bring in separately to test laws.

I don't think any of these are the end of the world but I tend to on balance think they make life more difficult for users for a largely theoretical benefit.

joroKr21 commented 4 years ago

The logical conclusion of that approach would be to extract assertions and validations to a mini library. After all there is no reason for them to depend on zio.

adamgfraser commented 4 years ago

That is possible.

The assertions themselves actually do have a reason to depend on ZIO because they describe both effectual and pure assertions. Of course you could split that but then you are taking one data type that unifies them and somewhat artificially splitting it up.

I think Validation is a pretty standard type that you expect in a library of functional programming abstractions and is one of the "easy wins" you get from using one. There is just a trade off here that at some point libraries get so small that it is less efficient for both users and maintainers to have them separate. Not having Validation as part of ZIO Prelude feels that way to me but that is just my opinion.

rnd4222 commented 4 years ago

@adamgfraser thanks for your response. Does it makes sense to make a very basic package (say, zio-prelude-kernel) which doesn't have any dependencies, and depend on it in a zio-prelude?

hmemcpy commented 2 years ago

Would like to bump this, as I've seen comments/concerns about prelude depending on ZIO core. In our case this is certainly a blocker for using in certain scenarios (we'd like to keep our core domain pure of any IO effects, but Validation and ZPure would be very useful)

What are the blockers for removing direct dependency on ZIO?

adamgfraser commented 2 years ago

Personally I view this as a non-goal.

While certainly possible, this would have the following disadvantages:

  1. All instances For ZIO data types would become orphan instances, since they could not be defined inside the data type companion objects in ZIO or the type class companion object in ZIO Prelude. This would require them to be imported to be available which would go against one of our major design goals of not having to do imports to get access to instances.
  2. All operators that interoperate with ZIO would have to become extension methods, which also goes against one of our design goals of using actual methods on data types as opposed to extension methods whenever possible. If anything I think we want to have more methods like this (e.g. specialized operators for recursive data types to do various transformations with ZIO effects or STM effects).
  3. More subtly, there are a variety of cases where ZIO data types such as Chunk or NonEmptyChunk are used internally (e..g in representing the potentially multiple errors in Validation). While these could certainly be replaced by List and :: that would also go against our design goals of consistently using data types like Chunk throughout.

I think the answer if you don't want to use effects is just don't use effects. Use Validation and don't use ZIO. This could potentially be augmented by static analysis tools but I think artificially separating ZIO and ZIO Prelude is not the way to go.

hmemcpy commented 2 years ago

I actually agree with most of your points, the alternative here is really to make all interop with ZIO via extension methods (.toZIO and friends) which could work, but is not ideal. Also, inverting the dependency is also a non-solution, because it goes against the goals of ZIO.

I feel that that the "lesser evil" here is possibly extracting a third library, like @rnd4222 suggested above - make a "zio-prelude-kernel" which would contain the base typeclasses, newtypes and assertions, and make fx and friends (i.e. everything that depends on ZIO) into zio-prelude proper. I think this way it can solve both issues of having a standalone prelude stdlib and extras for those who need it.

adamgfraser commented 2 years ago

I think this amounts to the same issues discussed above. It depends a little bit on what the scope is, but to the extent that type classes definitions don't depend on ZIO then instances for ZIO data types become orphans. To the extent that data types like Validation don't depend on ZIO then methods to interoperate with ZIO such as converting a Validation to a ZIO have to become extension methods and we can't use ZIO data types like Chunk.

I think the right way to look at this is ZIO is a base library and just like the Scala standard library provides some data types like Future or mutable collections that might not make sense in all parts of your code base. The right way to deal with that is just to not use those data types, not to say you don't want the dependency.