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

Bring back Applicative.pureEval or provide equivalent #1316

Closed
alexandru opened this issue Aug 22, 2016 · 17 comments
Closed

Bring back Applicative.pureEval or provide equivalent #1316

alexandru opened this issue Aug 22, 2016 · 17 comments

Comments

@alexandru
Copy link
Member

alexandru commented Aug 22, 2016

I see that pureEval was removed in this commit and deployed as part of version 0.7.0.

The explanation is fine, however it needs to be brought back or it needs an equivalent. There are two reasons for it:

  1. the point operation in Scalaz takes that parameter by name, which means Cats is no longer providing an equivalent to Scalaz's point
  2. I understand there were concerns about controlling of side-effects, but that's not the whole story. A lazy pure can also be used for actual lazy behavior, like when building a lazy generic data-structure; think of Streaming (that was moved in Dogs) or checkout this Enumerator type that I've been building. In other words, without a lazy pure, the Applicative type becomes insufficient for problems such as building a generic lazy list like Cons[F,A](head: A, tail: F[List[A]]) type
@alexandru
Copy link
Member Author

So a good alternative to pureEval would be adding a type like Suspendable (from fs2).

@travisbrown
Copy link
Contributor

👍 to a Suspendable in Cats.

@non
Copy link
Contributor

non commented Aug 22, 2016

👍 to Suspendable as well.

@johnynek
Copy link
Contributor

I'm negative on it being a subclass of Monad[F]. I think it will get into the same issues we have with ambiguous implicits when you do (implicit s: Suspendable[M], e: MonadError[M, Throwable])

Can it possibly take def delay(a: => A)(implicit m: Applicative[M]): M[A]

@johnynek
Copy link
Contributor

Also, I don't see why we want a: => A over Eval[A], which allows the caller to control if they want it once or many times.

With a: => A and the pledge to call each time, you can get the same control by doing, as @non suggests, val once = Eval.later(foo); delay(once.value), to me fundamentally accepting Eval is exactly what we want when we want the caller to control the evaluation strategy.

Next, it seems like this method becomes type equivalent to apply[T](ea: Eval[A]): M[A] which, of course, is Eval ~> M.

I am pretty concerned that we have something that on a type level is equivalent to Eval ~> M and a lack of clear laws about what is going on. It seems like the main argument for giving it a name is so that we can somehow warn people that something weird is going on, but we don't all share an understanding of what that is.

For instance, I made a PR in iteratee that leverages Eval and MonadError for this:

travisbrown/iteratee#106

I don't see an issue with the approach there.

@alexandru
Copy link
Member Author

alexandru commented Aug 24, 2016

@johnynek the Suspendable type I pasted from FS2 is just an example and can surely be made to inherit from Applicative, I have no problem with that.

I would also agree to having delay take an Eval as a parameter. However this choice I don't like so much.

The Eval type is really cool, the idea, the execution, it's certainly a useful type, but its proliferation in Cats' type-classes can hurt the design of these type-classes (personal opinion). In a way I'm glad that it is gone from Applicative. Also in my opinion the type-classes should strive to use standard data types, because we can have the story of Either vs Xor repeated.

The problem right now is that Eval is not the only type in the Scala ecosystem that is capable of delaying evaluation and side-effects, or that is implemented with a trampoline, or that can do memoization or that can replace lazy val or Function0 in code. Off the top of my head, there's TailCalls from Scala's standard library, Name / Need and Free.Trampoline from Scalaz, along with their Future / Task of course. And there's also the Monix Coeval and Task and the FS2 Task as well.

And the problem is that currently there is no type-class that describes Eval.always, Eval.later and Eval#memoize, even though these 3 operations can be abstracted and reused in implementations that can make use of the aforementioned types, the kind of implementation that abstracts over the underlying F[_] monad that drives the evaluation, like iteratee.io is doing.

Again, just a personal opinion, you folks have way more design experience than I do, but personally I would have preferred:

trait Suspendable[F[_]] extends Applicative[F] {
  def suspend[A](fa: => F[A]): F[A]

  def delay[A](a: => A): F[A] = 
    suspend(pure(a))
}

trait Memoizable[F[_]] extends Suspendable[F] {
  def memoize[A](fa: F[A]): F[A]

  def delayOnce[A](a: => A): F[A] =
    memoize(delay(a))
}

@alexandru
Copy link
Member Author

Some more thoughts:

  • Notice how memoize cannot be described with an Eval ~> M
  • Just because side-effects are involved, it doesn't mean that it cannot have laws - you only need an extra dependency to trigger the evaluation, e.g. if you also have that type be a Comonad, you could describe those laws, it's only unfortunate that we aren't currently describing laws like that
  • Not having laws does not negate the usefulness of having a type-class. There's a lot of value in having just a name and a Scaladoc.

@johnynek
Copy link
Contributor

Can we give some concrete implementations of this type class so we can see
some properties those concrete examples have that we want.

I'd like to see examples that are distinct from catchNonFatal on
MonadError. None immediately come to mind for me. What are Suspendable but
not MonadError and how would we use such instances?

I think abstraction works best when we see several cases it covers. I'd
like to see the several cases for this abstraction.

For instance, must delay be lazy? So it is impossible to implement
Suspendable[Try]?
On Tue, Aug 23, 2016 at 21:31 Alexandru Nedelcu notifications@github.com
wrote:

Some more thoughts:

  • Notice how memoize cannot be described with an Eval ~> M
  • Just because side-effects are involved, it doesn't mean that it
    cannot have laws - you only need an extra dependency to trigger the
    evaluation, e.g. if you also have that type be a Comonad, you could
    describe those laws, it's only unfortunate that we aren't currently
    describing laws like that
  • Not having laws does not negate the usefulness of having a
    type-class. There's a lot of value in having just a name and a Scaladoc.


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
#1316 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAEJdvTxqJUKhMO1OM6MWK8g1rdtvUyjks5qi_NQgaJpZM4Jp2Jk
.

@travisbrown
Copy link
Contributor

@johnynek It wouldn't be possible to give Try a Suspendable instance—you'd need Free[Try, ?].

There's a real difference between contexts with Suspendable instances (like the two Tasks, Eval, etc.) and those without (Try, Xor), and this has been my discomfort with travisbrown/iteratee#106 (and Eval in these APIs more generally)—it's papering over that difference in a way that seems ad-hoc and unnecessary when there's a perfectly good alternative (lift your non-Suspendable contexts into Free).

@johnynek
Copy link
Contributor

But that is only perfectly good if you don't care about the performance
cost of doing so. In the iteratee example you need to lift the whole
computation into a trampolined monad rather than just doing so for the
initial element.
On Wed, Aug 24, 2016 at 04:42 Travis Brown notifications@github.com wrote:

@johnynek https://github.com/johnynek It wouldn't be possible to give
Try a Suspendable instance—you'd need Free[Try, ?].

There's a real difference between contexts with Suspendable instances
(like the two Tasks, Eval, etc.) and those without (Try, Xor), and this
has been my discomfort with travisbrown/iteratee#106
travisbrown/iteratee#106 (and Eval in these
APIs more generally)—it's papering over that difference in a way that seems
ad-hoc and unnecessary when there's a perfectly good alternative (lift your
non-Suspendable contexts into Free).


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
#1316 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAEJdjjdWFbPEJYklwjcojL6al5gwwNhks5qjFhkgaJpZM4Jp2Jk
.

@adelbertc
Copy link
Contributor

adelbertc commented Aug 24, 2016

I'm negative on it being a subclass of Monad[F]. I think it will get into the same issues we have with ambiguous implicits when you do (implicit s: Suspendable[M], e: MonadError[M, Throwable])

#1210 strikes again.

It seems we keep trying to work around this. It happened a bit in MonadRec and it seems to be happening again now. We should either try to solve the ambiguous implicits issue with our type class encoding (and thereby making MTL much easier to use), or not cut corners to work around the problem.

@johnynek
Copy link
Contributor

What about something like this:

  /**
   * This is a FunctionK[Eval, F] with the contract that `.value`
   * is not called to lift from `Eval` into `F`.
   * so a test would be:
   *
   * var called = false
   * val e = Eval.later(called = true)
   * val sus: Suspendable[F] = ...
   * sus(e)
   * called == false
   */
trait SuspendableEval[F[_]] extends FunctionK[Eval, F] {
  def apply[T](e: Eval[T]): F[T]

  def suspend[T](f: => F[T])(implicit m: FlatMap[F]): F[T] =
    m.flatten(apply(Eval.always(f)))

  def captureEffect[T](t: => T): F[T] =
    apply(Eval.always(t))
}

@johnynek
Copy link
Contributor

actually, with that suspend, who says flatten does not force the value. Maybe that should not be there.

@alexandru
Copy link
Member Author

alexandru commented Sep 5, 2016

@johnynek I've thought more about this. The question is - who says that flatten or suspend should not force the value?

I was thinking that we also need to express the ability to "capture effects", otherwise this type-class would be less applicable as we also need to abstract over Try.apply and Future.apply, where apply(throw ex) == raiseError(ex). We could have expressed this with the old pureEval, but not anymore.

So forgetting about suspend for a moment, a good start would be a type like this:

trait Evaluable[F[_]] extends Applicative[F] {
  def captureEffect[A](t: => A): F[A]

  def eval[A](e: Eval[A]): F[A] =
    captureEffect(e.value)
}

Not sure how to express laws for it though. One good law is the one I mentioned, but we can't assume F is going to be an ApplicativeError[_, Throwable], as that would exclude types like Eval. And here we've got a limitation in how laws are expressed in Cats I think. Like we should be able to say that if F is an ApplicativeError[F,Throwable] then this law should happen.

Another law would be the equivalence with pure. It's not much, but it is something.

@non
Copy link
Contributor

non commented Sep 5, 2016

@alexandru I think it's possible to write laws like that -- we just don't have any right now. But I think it's totally valid to talk about laws that involve two type classes (and maybe we even have some in cats-kernel or algebra).

@johnynek
Copy link
Contributor

johnynek commented Jul 3, 2019

Note #2279 pretty much solves this. If you have Defer and Applicative you can implement pureEval.

@alexandru
Copy link
Member Author

Thanks @johnynek

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

6 participants