-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Optimize Alternative (part 2): add prependK/appendK specializations for std containers #4052
Optimize Alternative (part 2): add prependK/appendK specializations for std containers #4052
Conversation
Seems there's no laws nor tests suites for Zip* wrappers. Neither there're docs for them. I'm wondering – are these wrappers used somewhere? |
You mean like this?
|
Yeah, I didn't notice this test because it resides inside For example, this PR adds specializations for Furthermore, both |
IIUC the As an analogy: suppose I want a semigroup for integers. Both Similarly, we have Apply[List].product(List(1, 2), List('a', 'b'))
// List((1,a), (1,b), (2,a), (2,b))
Apply[ZipList].product(ZipList(List(1, 2)), ZipList(List('a', 'b'))).value
// List((1,a), (2,b)) Try it out: https://scastie.scala-lang.org/zqPsMxHmTSe2qhIY4yIjyQ This is exactly the basis for cats/core/src/main/scala/cats/instances/list.scala Lines 246 to 258 in 8787d59
|
we should be testing the |
I wonder if we can add this to the laws for |
yeah, it seems we should be testing that the |
Parallel is kind of weird typeclass... it's like here are two type constructors... they have an isomorphic Functor and pure, but all bets are off when it comes to the Apply instances and one is maybe "parallel" (but not necessarily, because |
Just because |
I guess I just mean the typeclass is underdefined... I think when we actually use it we almost always actually know the concrete type. Like, write an interesting abstract function using just I think it is a bit of a false generality.... Like, you can use Parallel with Does that make sense? |
Oh, it is absolutely underdefined. But I wonder if we can do better? I feel like I have an intuitive understand of what "parallel" means, and I wouldn't necessarily say it is a false generality. Something about how the "parallel" But maybe I'm just babbling. Also not sure if this reasoning generalizes to Edit: alas, I see my "no-consistent-Monad" reasoning has already excluded the |
I am struggling to understand, why does def pure[A](x: A): ZipLazyList[A] = new ZipLazyList(LazyList.continually(x)) It is definitely impossible to implement for strict collections like |
@satorg I think you need that definition of pure to satisfy this law: cats/laws/src/main/scala/cats/laws/ApplicativeLaws.scala Lines 13 to 14 in 309d344
Makes sense about the strict collections 👍 |
Right, but I guess this law cannot be verified if Along with |
FYI: I found out that seems such behavior was first introduced in this PR: #1938 for |
I could be mistaken, but I'm pretty sure you can, so long as |
I did already actually... |
|
Ah, yeah, I see what you mean – that is correct for sure if we talk about But unfortunately there're also def applicativeHomomorphism[A, B](a: A, f: A => B): IsEq[F[B]] =
F.pure(f).ap(F.pure(a)) <-> F.pure(f(a))
def applicativeUnit[A](a: A): IsEq[F[A]] =
F.unit.map(_ => a) <-> F.pure(a) |
Yup, those laws would definitely hang :) I think if you filter the tests to remove those troublesome laws, at least we can test the rest? |
Oh, here's one more idea: maybe you can override |
Yes, it works for implicit def limitedZipLazyListEq[A: Eq]: Eq[ZipLazyList[A]] = Eq.by(_.value.take(100))
checkAll("ZipLazyList[Int]", AlternativeTests[ZipLazyList].alternative[Int, Int, Int]) The only concern here is that the limit value (100) is hardcoded. It could be solved by generating it though, but it is a minutiae for now. The more important thing is that since this particular PR is about the implicit def limitedZipLazyListEq[A: Eq]: Eq[ZipLazyList[A]] = Eq.by(_.value.take(100))
checkAll("ZipLazyList[Int]", AlternativeTests[ZipLazyList].alternative[Int, Int, Int]) One of the Alternative's laws (namely
|
Seems that |
Yes, it seems to me that cats/laws/src/main/scala/cats/laws/NonEmptyAlternativeLaws.scala Lines 13 to 14 in 309d344
Suppose |
Yes, agree. I would guess it may not be lawful even in theory. I'm struggling to imagine how |
Btw, I've also found another piece of weirdness in // Can't test Parallel here, as Applicative[ZipLazyList].pure doesn't terminate
checkAll("Parallel[LazyList]", NonEmptyParallelTests[LazyList].nonEmptyParallel[Int, String])
checkAll("Parallel[NonEmptyLazyList]", ParallelTests[NonEmptyLazyList].parallel[Int, String]) The thing is that there's no overridden implicit def catsDataParallelForNonEmptyLazyList: Parallel.Aux[NonEmptyLazyList, OneAnd[ZipLazyList, *]] =
new Parallel[NonEmptyLazyList] {
type F[x] = OneAnd[ZipLazyList, x]
def applicative: Applicative[OneAnd[ZipLazyList, *]] =
OneAnd.catsDataApplicativeForOneAnd(ZipLazyList.catsDataAlternativeForZipLazyList)
...
} I.e. implicit def catsDataApplicativeForOneAnd[F[_]](implicit F: Alternative[F]): Applicative[OneAnd[F, *]] =
new Applicative[OneAnd[F, *]] {
....
def pure[A](x: A): OneAnd[F, A] = OneAnd(x, F.empty)
override def ap[A, B](ff: OneAnd[F, A => B])(fa: OneAnd[F, A]): OneAnd[F, B] = {
val (f, tf) = (ff.head, ff.tail)
val (a, ta) = (fa.head, fa.tail)
val fb = F.ap(tf)(F.combineK(F.pure(a), ta))
OneAnd(f(a), F.combineK(F.map(ta)(f), fb))
}
} I.e. it redefines But the problem there is that all checks from WDYT? |
Can we summarize the issues here?
I think we have an issue that we have instances that seem untested. And another issue about Parallel possibly being either ill-defined or under-defined. |
Nice summary. IMO:
Yes, I have more thoughts about |
I wonder if something like this Well, I guess if |
Hmm... |
If it doesn't break any laws, but the current ones do, I would argue it isn't suspicious. Basically this is point wise aligning items, which feels like parallel to me. If that passes the distributive laws, I don't see any issue with it. Also, note it follows |
It looked strange to me too at first but I think it might work actually :) I think maybe before this PR, we should have another PR that:
Then this PR can stay focused on its original mission to implement |
Yeah, that makes sense to me too. I'd feel more confident about this PR if there were some tests for the code before the changes and I could see that the changes wouldn't break the tests. I'll take on this. One more thing to confirm: as I understand, it is not possible to implement I mean, apparently |
@satorg Thanks! Sounds great. Yup, that's exactly my understanding about strict collections. BTW, I just saw this relevant issue: Actually, I'm not sure I agree with it. I am writing up an issue about |
I think we invented Parallel in cats, so we are in charge of what it means. It isn't entirely clear what the laws should be. I think that is worth exploring in more detail. I think it was basically developed to abstract over Either <-> Validated and IO <-> IO.Par. Basically those where the motivating use cases, IIRC. We could have overfit on those two with some of the laws. But since it is so vague now what we mean it is hard to say. Basically the idea is sometimes there are multiple different applicatives that could be defined, but in those cases some of them cannot be extended to a Monad. I'm not sure if it is much deeper than that. |
530006f
to
efa971c
Compare
@armanbilge I decided to remove specializations for Zip collections from this PR since they apparently are worth a separate PR. Meanwhile it makes this PR focused on specializations for standard collections only and I think it is in a pretty good shape now, wdyt? |
@@ -16,6 +16,8 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances { | |||
|
|||
def combineK[A](x: Stream[A], y: Stream[A]): Stream[A] = x #::: y | |||
|
|||
override def prependK[A](a: A, fa: Stream[A]): Stream[A] = a #:: fa |
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 LazyList
you overrode both prependK
and appendK
, why only prependK
for Stream
, are the implementations different?
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.
Right, this is because appendK
for LazyList
utilizes LazyList#appended
which is optimized for LazyList
. But appended
/prepended
methods were first introduced in Scala 2.13 and Stream
in Scala 2.12 does not have such methods yet. Therefore seems the best we can do here is to call
def appendK[A](fa: Stream[A], a: A) = fa #::: Stream(a)
but it is exactly what combineK
(along with pure
) already does.
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.
On the second thought, Stream
in Scala 2.13 does have the prepended
/appended
methods, but as I can see they're simply come from Seq
and aren't optimized for Stream
(unlike those in LazyList
).
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.
Makes sense, thanks for explaining!
See initial PR #4014 for details.
Subsequent PRs:
This PR adds optimized specializations for the new
prependK
/appendK
methods introduced in the initial PR.