Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@gvolpe
Copy link
Contributor

@gvolpe gvolpe commented Mar 3, 2019

Now that ZIO became a trifunctor, interop instances should also take into account the environmental parameter R as well. Here's the summary of changes:

  • A new type alias type TaskR[-R, +A] = ZIO[R, Throwable, A] was introduced.
  • ~All existing tests except for one (SemigroupK[UIO[?]]) are passing. ~ I replaced it with both Task and TaskR. Does UIO[?] make any sense in this test?
  • In some places where effects needs to be evaluated is necessary to convert R into Any via asInstanceOf, not sure if that's the best way. βœ…

TODO

  • Fix (or delete if correct) the commented test.
  • Add more tests summoning instances using different environmental parameters.

Consider this PR a proposal to further discuss the topic rather than a final solution :)

}

private class CatsConcurrentEffect extends CatsConcurrent with effect.ConcurrentEffect[Task] {
private class CatsConcurrentEffect[R] extends CatsConcurrent[R] with effect.ConcurrentEffect[TaskR[R, ?]] {
Copy link
Member

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...

Copy link
Member

@neko-kai neko-kai Mar 3, 2019

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.

Copy link
Contributor Author

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.

Copy link
Member

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 R for 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.DefaultRuntime

How does this sound?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jdegoes I think @Kaishh meant R=Any for Effect and ConcurrentEffect. All the instances up to Concurrent are now fully polymorphic on R.

With regards to the runtime, I'll leave that decision to you both that are more familiar with the design :)

Copy link
Member

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.

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 4, 2019

@jdegoes thanks for reviewing. I mentioned the use of asInstanceOf in the description, I couldn't find another way to make it compile but given @Kaishh 's input I think I can make it work without the hack. Allow me some time to get into it :)

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 4, 2019

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

@Kaishh I made the effect instances take an implicit runtime: Runtime[Any], is that what you meant?

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 4, 2019

The CI is complaining about formatting but I ran scalafmt and the tests locally without any issues.

[error] /home/travis/build/scalaz/scalaz-zio/interop-cats/jvm/src/test/scala/scalaz/zio/interop/catzSpec.scala isn't formatted properly!
[error] (interopCatsJVM / Test / scalafmtCheck) /home/travis/build/scalaz/scalaz-zio/interop-cats/jvm/src/test/scala/scalaz/zio/interop/catzSpec.scala isn't formatted properly!
[error] Total time: 5 s, completed Mar 4, 2019 5:29:13 AM

I've run scalafmt once again but there are no changes. Is there any other command I should run? Or maybe restarting the CI build could work?

@ghostdogpr
Copy link
Member

@gvolpe I restarted the Lint task but same result. Did you run sbt fmt from the root directory?

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 4, 2019

Thanks @ghostdogpr , that did the trick. I was running just scalafmt.

@neko-kai
Copy link
Member

neko-kai commented Mar 4, 2019

@gvolpe

I made the effect instances take an implicit runtime: Runtime[Any], is that what you meant?

No, the point is to allow injection of ANY R, which would be the behavior most useful to someone using Reader R, but as long as that R cake has a Runtime we can use to run the IO. One of R <: Runtime[R], R <: Runtime[_], R <: Platform or R <:< Runtime[R] or [R](implicit rts: R with Runtime[R]) should be able to express that.

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 4, 2019

@Kaishh do we really need to have Runtime[R] for the typeclasses up to Concurrent? I was thinking that in order to run your TaskR[R, ?] you need to provide the R first, in which case Runtime[Any] will do just fine.

Now if you think Runtime[R] makes sense is there any other way to provide it instead of mixing it in the cake? I'd try to avoid this at all cost because it wouldn't work with classy optics I think, which is what I'm aiming for (and got already working in a demo project).

How about [R](implicit rts: Runtime[R])? No mixing needed.

@neko-kai
Copy link
Member

neko-kai commented Mar 4, 2019

@gvolpe

do we really need to have Runtime[R] for the typeclasses up to Concurrent?

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.

How about [R](implicit rts: Runtime[R])? No mixing needed.

Ok, I guess that makes the most sense if you're ok with it.

@ghostdogpr
Copy link
Member

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).

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 5, 2019

@jdegoes I believe I addressed all your comments.

@ghostdogpr maybe the docs part can be another PR?

@ghostdogpr
Copy link
Member

@ghostdogpr maybe the docs part can be another PR?

Sure!

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 5, 2019

An unrelated test is failing on the CI build (no issues locally):

[error] Failed tests:
[error] 	scalaz.zio.stream.StreamSpec

@ghostdogpr
Copy link
Member

@gvolpe I relaunched and it passed πŸ‘

Copy link
Member

@jdegoes jdegoes left a 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)
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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] = this

If I remove the with DefaultRuntime how do I get a runtime? This class is instantiated by the test framework AFAIU.

Copy link
Member

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 {}

Copy link
Member

@jdegoes jdegoes Mar 7, 2019

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.

Copy link
Contributor Author

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 :)

Copy link
Contributor Author

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 😞

Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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(
Copy link
Member

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
Copy link
Member

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

neko-kai
neko-kai previously approved these changes Mar 5, 2019
@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 5, 2019

@jdegoes Also what do you think about renaming TaskR to RIO? It'll be like the Haskell implementation.

Copy link
Member

@jdegoes jdegoes left a 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!

@gvolpe
Copy link
Contributor Author

gvolpe commented Mar 8, 2019

@jdegoes I just removed all the property tests previously added with the new parameter R and left the Eq instances as they were more or less.

Instead I added some code that summons the Cats instances that have an environmental parameter. This should do I hope.

neko-kai
neko-kai previously approved these changes Mar 8, 2019
Copy link
Member

@neko-kai neko-kai left a comment

Choose a reason for hiding this comment

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

Looks great πŸ‘

@jdegoes jdegoes merged commit f70a6e3 into zio:master Mar 9, 2019
@jdegoes
Copy link
Member

jdegoes commented Mar 9, 2019

giphy

Congratulations, & thanks for your contribution!

@gvolpe gvolpe deleted the feature/trifunctor-cats-instances branch March 9, 2019 11:12
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.

4 participants