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

We need a toIO operation and type class #73

Closed
alexandru opened this issue Aug 6, 2017 · 5 comments
Closed

We need a toIO operation and type class #73

alexandru opened this issue Aug 6, 2017 · 5 comments

Comments

@alexandru
Copy link
Member

alexandru commented Aug 6, 2017

We have LiftIO as a lawless type class that's then inherited by Async.
I think we also need a ToIO type class to be inherited by Effect:

trait ToIO[F[_]] {
  def toIO[A](fa: F[A]): IO[A]
}

trait Effect[F[_]] extends Async[F] with ToIO[F] {
  // ...
}

Reasons:

  1. when abstracting over Effect, going through toIO for evaluating an F[_] might actually be easier to do than going through runAsync, we can already express toIO in terms of runAsync so all Effect instances can do it
  2. implementations can provide an efficient toIO and we already have precedent for such operations in Cats, e.g. Applicative.unit

But I'd also like a different ToIO type class because there are data types out there that can be converted to IO, but that can't implement Effect. Not sure if it can have any laws though.

If there's agreement I can work on the PR.
So what do you think?

@djspiewak
Copy link
Member

djspiewak commented Aug 13, 2017

So the problem with this is, taken together with LiftIO (as would be the case for any Effect), it means that implementors can only be exactly as powerful as IO. In other words, Effect[F] would be equivalent to F <-> IO, where <-> is defined as follows:

trait <->[F[_], G[_]] {
  def left: F ~> G
  def right: G ~> F
}

That's a very, very constraining type. With that said though, your argument that toIO can be implemented via runAsync is perfectly true, and that bothers me quite a lot. It's part of why nearly all of the inductive instances provided by cats-effect must produce an Async rather than an Effect. For example, you cannot automatically derive an Effect[OptionT[F, ?]] given Effect[F] without "throwing" an exception; and you cannot automatically derive an Effect[StateT[F, S, ?]] given Effect[F] without Monoid[S], and even then it probably doesn't have the properties you would want.

So maybe the isomorphism is a fair way to describe things: all base effect types (i.e. non-MTL) must be directly isomorphic to IO. I just don't want to under-sell what a strong claim that is. LiftIO says that any Async must be at least as powerful as IO; ToIO would say that any Effect must be exactly as powerful as IO (no more or less).

If we're going to have this, I would actually rather have a more general version:

trait ToAsync[F[_]] {
  def to[G[_]: Async, A](fa: F[A]): G[A]
}

Effectively generalizing the IO#to function.

Stepping away from the pros and cons for a minute, do you have any concrete use-cases that this would aid (either in the generic or the specific toIO formulation)?

@alexandru
Copy link
Member Author

alexandru commented Aug 15, 2017

Effect indeed describes types that are exactly as powerful as IO, however it does so only because it inherits from Async. What I predict we'll need sometime is the ability to work with any datatype that could support an unsafeRunAsync operation, but without the ability to implement Async.

unsafeRunAsync is a very generic operation, although it does add constraints of its own.:

  1. the callback you give to unsafeRunAsync can only be called once, so when evaluated such a reference needs to produce a single value (versus many)
  2. the data type does not need the the ability to signal errors, but when it has that ability, it probably implements ApplicativeError[Throwable, ?]

Examples of data types that are not Async, but that could implement this operation are cats.Eval and scala.concurrent.Future. We can't describe these laws very well and as you've said, it's an unsafe operation, so I could live with a generic conversion of such types to IO.

Use case

I built a new data type in Monix called Iterant, making use of higher-kinded polymorphism for evaluation. I've been told that it resembles the efforts for "ListT done right", or what FS2 did years ago; I wouldn't know, because I've been living under a rock 🙂

So one of its operators is mapEval, with a signature like this:

sealed trait Iterant[F[_], A] {
  // ...
  def mapEval[B](f: A => F[B])(implicit F: Sync[F]): Iterant[F, B]
}

Here we need F: Sync[F] because F is driving the evaluation, so it needs to have a memory safe flatMap and to do error handling, but it does not need to implement Async or Effect.

But to keep the operators consistent and because mapEval is nice to have, I've also added it to Observable and this time it looks like this:

trait Observable[+A] {
  //...
  def mapEval[F[_], B](f: A => F[B])(implicit F: Effect[F]): Self[B]
}

But requiring Effect here is too much. The interesting difference between Observable and Iterant is that Observable itself does the evaluation, not needing a memory safe F[_] data type that can suspend side effects.

Therefore in this mapEval for Observable:

  1. I don't care about the memory safety of Sync's flatMap
  2. I don't care about suspending side effects or recovering from errors (as long as errors get signaled via unsafeRunAsync), because it's the Observable that takes care of it
  3. I don't care about building Async instances of that F

All I would care about in this instance is to have an unsafeRunAsync and that's it. Which is why there's no reason to restrict this operation to F: Effect data types, when cats.Eval or scala.concurrent.Future and maybe others would work just fine.

This is why I would be perfectly fine with requiring a data type that's able to convert to IO. Your ToAsync suggestion doesn't sound bad either.

@alexandru
Copy link
Member Author

In contrast with the above use case, an example where I actually need an F[_] : Effect with all its properties is in my "Reactive Streams" spec integration, where I'm implementing an org.reactivestreams.Publisher that triggers the evaluation on .subscribe(), if interested see: monix/monix#417

@mpilquist
Copy link
Member

@alexandru Okay to close?

@alexandru
Copy link
Member Author

Yes

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

No branches or pull requests

3 participants