Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further Generalize ZPure #461

Merged
merged 21 commits into from
Jan 14, 2021
Merged

Further Generalize ZPure #461

merged 21 commits into from
Jan 14, 2021

Conversation

adamgfraser
Copy link
Contributor

This PR explores further generalizing ZPure in two ways:

  1. Adding an additional type parameter W representing a log that can be written to in the course of executing the computation
  2. Adding a concept of being able to accumulate multiple errors through the use of a zipWithPar operator that would not do actual concurrency but would perform both computations, even if the first one failed, and accumulate all errors in a Cause data structure.

The idea is that with these changes we would be able to delete the Validation data type and it would just become:

type Validation[+E, +A]      = ZPure[Nothing, Unit, Unit, Any, E, A]
type ZValidation[+W, +E, +A] = ZPure[W, Unit, Unit, Any, E, A]

ZPure would also be able to model the concept of non-fatal errors through the W type, potentially allowing These to be a more unbiased data representing the concept of either or both versus explicitly modeling non-fatal errors.

override def tag: Int = Tags.Log
}

private final case class GetLog[W, S]() extends ZPure[W, S, S, Any, Nothing, Chunk[W]] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the optimal encoding of getting the log.

@adamgfraser
Copy link
Contributor Author

@jdegoes This is ready for another review.

* `Cause` is a data type that represents the potentially multiple ways that a
* computation can fail.
*/
sealed trait Cause[+E] { self =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more general than just failure. Especially if we add an empty, it becomes a free semiring. It maybe should live in zio.prelude directly and not speak about errors, just "events" or something, in parallel and sequence. It can be used for errors but could be used for tests and other things. Anywhere you need a tree of parallel / sequential operations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could definitely generalize it and then just have Cause be a type alias for it.

@adamgfraser
Copy link
Contributor Author

@jdegoes I generalized Cause to Semiring. There is definitely more work I can do there to build that out but I think it may make sense to do that separately as Semiring now has all the functionality currently needed by ZPure and this PR is already quite large.

@adamgfraser
Copy link
Contributor Author

@jdegoes I added a second type parameter to Semiring to represent the empty type. Would appreciate your feedback when you have the chance. It does let us abstract over emptiness but there are a couple of places where the results are not ideal.

* return a new collection of events that represents this collection of
* events in parallel with that collection of events.
*/
final def &&[Z1 >: Z <: Unit, A1 >: A](that: Semiring[Z1, A1]): Semiring[Z1, A1] =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're losing information right now that combining an empty collection with a nonempty collection results in a nonempty collection. Potentially we could could use overloading or type level programming to recover this.

* events, collecting them back into a single collection of events.
*/
final def flatMap[Z1 >: Z <: Unit, B](f: A => Semiring[Z1, B]): Semiring[Z1, B] =
fold(Semiring.empty, f)(_ ++ _, _ && _).asInstanceOf[Semiring[Z1, B]]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know that the resulting collection can only be empty if Z or Z1 is Unit but we still have to handle the case where the initial collection is empty so we have to cast here.

* events.
*/
final def foreach[F[+_]: IdentityBoth: Covariant, B](f: A => F[B]): F[Semiring[Z, B]] =
fold[F[Semiring[Unit, B]]](Semiring.empty.succeed, a => f(a).map(Semiring.single))(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar issue as above.

}
}

case object Empty extends Semiring[Unit, Nothing]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pattern matches against Empty don't work well because it is not known to be a valid subtype if Z is abstract, since Z could be Nothing. Maybe if we go down this route we should make the constructors private and only expose them through folding and the operators we provide?

@@ -516,38 +541,42 @@ sealed trait ZPure[-S1, +S2, -R, +E, +A] { self =>
final def run(s: S1)(implicit ev1: Any <:< R, ev2: E <:< Nothing): (S2, A) =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the additional functionality we are adding there is a risk of having a ton of different ways to run a ZPure value. Also, I would like to improve the ergonomics in the many cases where there is no state type.

I was thinking about trying to make the following the primary method to run ZPure values:

final def run(implicit ev1: Unit <:< S1, ev2: Any <:< R, ev3: E <:< Nothing): A

Then we could have a runLog variant that exposes the value and the log. Users would provide the input state through a provideState operator if they had not already set an initial state, similar to how you provide the environment to a ZIO effect before running it, and would normally handle their errors before running it, even by just doing zPure.either. We could perhaps have a runEither variant and maybe a runState variant for when you are just doing state but I would like to avoid users having to always provide a Unit state type when running computations and having every possible variant of state, logging, error, and value outputs.


object Semiring {

final case class Both[+Z <: Unit, +A](left: Semiring[Z, A], right: Semiring[Z, A]) extends Semiring[Z, A] { self =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially make this:

final case class Both[+A](left: Semiring[Nothing, A], right: Semiring[Nothing, A]) extends Semiring[Nothing, A]

Then in ++ and && we could just ignore empty values in composition. Basically say that if there is an empty element it has to occur at the root, there can't be empty elements at arbitrary nodes in the tree.

@jdegoes
Copy link
Member

jdegoes commented Dec 18, 2020

Ready to merge, although let's change the name Semiring to ParSeq or something similar. We can figure out a better name later.

@jdegoes jdegoes merged commit bbf4068 into zio:master Jan 14, 2021
@adamgfraser adamgfraser deleted the zpure branch January 25, 2021 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants