-
Couldn't load subscription status.
- Fork 1.4k
Cats instances taking the environmental parameter #593
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
Conversation
| } | ||
|
|
||
| private class CatsConcurrentEffect extends CatsConcurrent with effect.ConcurrentEffect[Task] { | ||
| private class CatsConcurrentEffect[R] extends CatsConcurrent[R] with effect.ConcurrentEffect[TaskR[R, ?]] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this type class, I think we will have to hard code R to be some known set of environmental effects (say, all the effects supported by DefaultRuntime), so that we can provide them before we (for example) convert toIO.
Basically if you look at Cats ReaderT[R, F, ?], we can support all the cats effects type classes that it supports, without hard coding R; then for the rest, we need to pick an R. Either that or use implicits to "inject" an R.
Let's have @Kaishh take a look too since this could hugely improve usability of R for cats-effect users...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, saying [R >: DefaultRuntime] would allow an implementation of toIO - but it wouldn't be all that useful for someone trying to use R as Reader since they would be limited to only R >: DefaultRuntime <: Any - @gvolpe please correct me if I'm wrong. It would also invite incoherence, because the DefaultRuntime provided will NOT be the one created by user, but just CatsConcurrentEffect's own this!
The most principled solution I see would unfortunately decrease usability for cats-effect users :)
- Provide instances up to Concurrent for any
R - Provide an instance for ConcurrentEffect with Effect for
R <: Runtime[R]where implicit R is required and STOP extending DefaultRuntime since we can use the executor from implicit instead of creating a global
Another, more usable solution, is to simply provide *Effect only for R=Any, this means no implicit rituals and no incoherence in environment β at the cost of not being able to access CatsConcurrentEffect.this - but I don't think that's an awful loss.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jdegoes @Kaishh thanks for looking into it.
Provide instances up to Concurrent for any R
I think this makes sense and this is the way it's done in cats-effect. Instances for Kleisli can only be implemented up to Concurrent but somehow I thought having the Reader capabilities built in would allow to go further.
Another, more usable solution, is to simply provide *Effect only for R=Any
This seems to be the best solution given the first point :)
Will give it a spin later today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another, more usable solution, is to simply provide *Effect only for R=Any
Unless I'm missing something, I'm not a fan of this approach as it doesn't increase usability.
I really think that for up to Concurrent, we should be maximally polymorphic and support any R, because it has no impact on cats-effect users: if they're not using R (R =:= Any), then they can still benefit from the polymorphic instances; and if they are using R, they'll be grateful that "just works" out of the box with everything up to Concurrent βΒ and the only reason it doesn't work with classes beyond that is because of the run methods and toIO methods that will be going away in Cats Effect 2.0.
The "global injector" is interesting. Right now you have zero control over thread pool in Cats. You're stuck with the default one. No chance to use, for example, Scala's global execution context or an app-specific thread pool.
If we stop extending DefaultRuntime, then Concurrent and beyond could require an implicit Runtime[R], which allows a user to do two things:
- Use any
Rfor all the type classes - Configure the platform (thread pool, fatal errors, error reporter)
This comes at cost to adding an implicit, but we could make it easy, e.g.:
import scalaz.zio.cats.implicits.DefaultRuntimeHow does this sound?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, great. In that case, for Concurrent and beyond, my vote would be having those instances take an implicit Runtime[R] so we can make them all polymorphic in R, and then adding a global implicit so users can import to "unsafe run" stuff that just uses R = Console with Random with System with Blocking with Scheduler.
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
β¦ ConcurrentEffect. Fixing tests
@Kaishh I made the effect instances take an |
|
The CI is complaining about formatting but I ran I've run |
|
@gvolpe I restarted the |
|
Thanks @ghostdogpr , that did the trick. I was running just |
No, the point is to allow injection of ANY |
|
@Kaishh do we really need to have Now if you think How about |
No, we don't. I'm only talking about ConcurrentEffect and Effect, we don't need to run and don't need the runtime for non *Effect classes.
Ok, I guess that makes the most sense if you're ok with it. |
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
|
It would be nice to update the microsite as well with an example showing how to get an instance of ConcurrentEffect with this new way (the current doc doesnβt tell that you need an implicit clock). |
|
@jdegoes I believe I addressed all your comments. @ghostdogpr maybe the docs part can be another PR? |
Sure! |
|
An unrelated test is failing on the CI build (no issues locally): |
|
@gvolpe I relaunched and it passed π |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few minor comments (a few remaining unsafe casts that need to use provide) and a request for a "companion object" for TaskR. Otherwise looking great!
@Kaishh You want to take a look?
| val v1 = unsafeRunSync(io1.timeout(20.seconds)).map(_.get) | ||
| val v2 = unsafeRunSync(io2.timeout(20.seconds)).map(_.get) | ||
| def eqv(io1: ZIO[R, E, A], io2: ZIO[R, E, A]): Boolean = { | ||
| val v1 = unsafeRunSync(io1.asInstanceOf[IO[E, A]].timeout(20.seconds)).map(_.get) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to use provide
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do I get the R in this case? By creating an Arbitrary for R?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need an implicit runtime. Or you can provide equality for IO[E, A] or even Task[A] if the tests will permit it. I don't think the tests use anything except for Task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jdegoes as I mentioned on Gitter I'm still lost with this part. Adding an implicit rts: Runtime[R] constraint to derive the Eq instances makes sense but the issue is where to get this instance from? If you look at how the spec class is defined maybe you'll see the problem?
class catzSpec
extends FunSuite
with BeforeAndAfterAll
with Matchers
with Checkers
with Discipline
with TestInstances
with GenIO
with DefaultRuntime {
override val Platform = PlatformLive.makeDefault().withReportFailure(_ => ())
implicit val runtime: Runtime[Any] = thisIf I remove the with DefaultRuntime how do I get a runtime? This class is instantiated by the test framework AFAIU.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gvolpe you can just do implicit val runtime: Runtime[Any] = new DefaultRuntime {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to get the R from somewhere. Define an implicit def that for any type R: Arbitrary can return a Arbitrary[Runtime[R]]. Then you can easily summon a Runtime for any arbitrary R inside the test.
A more hacky way to do that is to define an implicit def that provides an inolicit Runtime for any Arbitrary R. By running the Gen to get some R.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The arbitrary trick is what I suggested at first but we were probably thinking of different things π Now it makes sense, will try to get it done! Thanks :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jdegoes Unfortunately I'm still stuck with this. The idea sounds good but it seems I require more than just Runtime[R]: I need Runtime[R with Clock with System .... ]. So this was my attempt:
implicit val anyArb: Arbitrary[Any] = Arbitrary(Gen.const(()))
type Env[R] = R with Clock with Console with System with Random with Blocking
implicit def deriveRuntime[R: Arbitrary]: Gen[Runtime[Env[R]]] =
Arbitrary.arbitrary[R].map { env =>
Runtime[Env[R]](runtime.Environment + env, PlatformLive.Default)
}
implicit def catsEQ[R: Arbitrary, E, A: Eq](implicit rts: Runtime[Env[R]]): Eq[ZIO[R, E, A]] =
new Eq[ZIO[R, E, A]] {
import scalaz.zio.duration._
def eqv(io1: ZIO[R, E, A], io2: ZIO[R, E, A]): Boolean = {
val v1 = rts.unsafeRunSync(io1.timeout(20.seconds)).map(_.get)
val v2 = rts.unsafeRunSync(io2.timeout(20.seconds)).map(_.get)
val res = v1 === v2
if (!res) {
println(s"Mismatch: $v1 != $v2")
}
res
}
}
implicit def catsParEQ[R: Arbitrary, E: Eq, A: Eq](implicit rts: Runtime[Env[R]]): Eq[ParIO[R, E, A]] =
new Eq[ParIO[R, E, A]] {
def eqv(io1: ParIO[R, E, A], io2: ParIO[R, E, A]): Boolean =
rts.unsafeRun(Par.unwrap(io1).either) === rts.unsafeRun(Par.unwrap(io2).either)
}In order to create the environment I need to mixin some traits (Runtime.apply) and all I've got is a plain value of type R so this doesn't seem possible.
Also notice that where I require an implicit rts: Runtime[Env[R]] I'm not going to get one. Instead I'll have a Gen in scope to create a Runtime[Env[R]] which seems overkill. Why not just casting the given ZIO[R, E, A] to IO[E, A] as I've done to get it working? After all it's just tests to unsafeRunSync some computation but if there's a better way I'd like to know it.
So once again I'm on a dead end here π
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any attempt to cast away the R will result in a runtime exception somewhere, unless the R is not actually used.
I see now that you cannot use an arbitrary R here. If you like, you could create a "test" module, like this one:
trait TestModule[+A] {
def testModule: A
}Then you could make an Arbitrary for that (given an Arbitrary for A), and finally, you could supply an Arbitrary[Runtime[TestModule[A] with Clock with System ...].
However, the question is why? In my opinion, testing with R = Int and R = String like is being done is not that useful. You may as well fix the environment to Clock with System or weaker in the tests, rather than using R = Int or R = String.
If you fix the environment to the above, you can just use implicit val TestRuntime = new DefaultRuntime. Then fix the two or so compiler errors where a test is using a different R. Then you should be good to go.
It's a nice idea in theory to make the test R agnostic but you can't use a single R for the whole test suite in that case, which will cause lots of pain. Just best to fix the R for the test suite, because it's already as polymorphic as it can be in the instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bearing with me, John.
However, the question is why? In my opinion, testing with R = Int and R = String like is being done is not that useful. You may as well fix the environment to Clock with System or weaker in the tests, rather than using R = Int or R = String.
You're right. I just want to verify that there are Cats instances in scope for TaskR[R, ?] so I could just add the code that summons such instances somewhere, don't need property tests. If something is wrong, the code just won't compile.
If you fix the environment to the above, you can just use implicit val TestRuntime = new DefaultRuntime. Then fix the two or so compiler errors where a test is using a different R. Then you should be good to go.
I don't understand this. Having a DefaultRuntime works fine for the other tests but what do you mean with "fix the compiler errors where a test is using a different R"? I think this is the whole issue and why I'm stuck here, it needs a Runtime[R] π
It's a nice idea in theory to make the test R agnostic but you can't use a single R for the whole test suite in that case, which will cause lots of pain. Just best to fix the R for the test suite, because it's already as polymorphic as it can be in the instances.
Completely agree π I just want to make this as simple as possible, I just haven't found the way.
| val v2 = unsafeRunSync(io2.timeout(20.seconds)).map(_.get) | ||
| def eqv(io1: ZIO[R, E, A], io2: ZIO[R, E, A]): Boolean = { | ||
| val v1 = unsafeRunSync(io1.asInstanceOf[IO[E, A]].timeout(20.seconds)).map(_.get) | ||
| val v2 = unsafeRunSync(io2.asInstanceOf[IO[E, A]].timeout(20.seconds)).map(_.get) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to use provide.
| implicit def catsParEQ[R, E: Eq, A: Eq]: Eq[ParIO[R, E, A]] = | ||
| new Eq[ParIO[R, E, A]] { | ||
| def eqv(io1: ParIO[R, E, A], io2: ParIO[R, E, A]): Boolean = | ||
| unsafeRun(Par.unwrap(io1.asInstanceOf[ParIO[Any, E, A]]).either) === unsafeRun( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to use provide
| new Eq[ParIO[R, E, A]] { | ||
| def eqv(io1: ParIO[R, E, A], io2: ParIO[R, E, A]): Boolean = | ||
| unsafeRun(Par.unwrap(io1.asInstanceOf[ParIO[Any, E, A]]).either) === unsafeRun( | ||
| Par.unwrap(io2.asInstanceOf[ParIO[Any, E, A]]).either |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to use provide
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
interop-cats/jvm/src/main/scala/scalaz/zio/interop/catsjvm.scala
Outdated
Show resolved
Hide resolved
|
@jdegoes Also what do you think about renaming |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nearly there! Just a few small changes and we'll be ready to merge and cut a release. This is awesome work and greatly improves the usability of R from cats-effects programs!
β¦for ZIO[R, E, A]
β¦roperty tests for R that did not make much sense and made it very difficult to test
|
@jdegoes I just removed all the property tests previously added with the new parameter Instead I added some code that summons the Cats instances that have an environmental parameter. This should do I hope. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great π
Now that
ZIObecame a trifunctor, interop instances should also take into account the environmental parameterRas well. Here's the summary of changes:type TaskR[-R, +A] = ZIO[R, Throwable, A]was introduced.SemigroupK[UIO[?]]) are passing. ~ I replaced it with bothTaskandTaskR. DoesUIO[?]make any sense in this test?In some places where effects needs to be evaluated is necessary to convertβRintoAnyviaasInstanceOf, not sure if that's the best way.TODO
Consider this PR a proposal to further discuss the topic rather than a final solution :)