-
-
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
Adding get for Foldable #1464
Adding get for Foldable #1464
Conversation
e77602d
to
cc8e7c9
Compare
Codecov Report
@@ Coverage Diff @@
## master #1464 +/- ##
==========================================
+ Coverage 93.38% 93.94% +0.55%
==========================================
Files 241 241
Lines 4054 4096 +42
Branches 141 156 +15
==========================================
+ Hits 3786 3848 +62
+ Misses 268 248 -20
Continue to review full report at Codecov.
|
* The default implementation of this is based on `foldLeft`, and thus will | ||
* always fold across the entire structure. | ||
*/ | ||
def get[A](fa: F[A])(idx: Long): Option[A] = |
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.
Has it not been investigated, whether it's possible to do this with foldRight
?
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.
If we are going to add this, I think we need to implement the faster thing for Vector, NonEmpty*, List, etc...
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.
@edmundnoble I don't think it's possible to do it with foldRight
unless we choose to have a mutable index outside of the loop, since the the foldRight
only 'pulls' A
rather than the accumulation.
I've got no personal objections to doing it with a mutable var
, but what are your thoughts?
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 the default implementation I think .foldLeft
is probably the best.
If you built a custom monadic data type with the right semantics I think you could use .foldM
to implement .get
with short-circuiting. But I'm not sure it's worth doing compared to just letting data types override the method if desired.
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 implementation now uses foldM
over Either
, looks good to me. 👍
/** | ||
* Get the element at the index of the `Foldable` | ||
*/ | ||
def apply[A](fa: F[A])(idx: Long): A = get(fa)(idx).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.
Not crazy about adding methods like this without an unsafe
warning. Something like getUnsafe
? (I can't recall what our unsafe convention is: prefix or suffix).
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.
I'm not quite sure that I agree with this, since the apply
/get
method is named the same as the scala collection methods. I quite like the behavior that in the particular case that we have a concrete type it would defer to that behavior.
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.
apply
is "special" because it is an unsafe method - I would prefer to have any/all unsafe methods in Cats tagged with some indication that it's unsafe in the name. Or better yet just not have it, and users can call .get
on get
if they want to be unsafe.
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.
I would agree for functions which aren't obviously unsafe - the unsafeRun
in the Stream
in fs2 comes to mind. However, for getting any index or key in a Map
or List
using apply
in scala is already implied shorthand for get.get
?
Since I would argue that users should already be familiar with this behaviour, I can't see much benefit in calling it getUnsafe
.
I don't feel particularly strongly enough to not change it, but I don't think to signal it's an unsafe method to users is a good enough reason.
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 to be clear - I'll change the name since any change to this ought to change the rest of the names for the sake of consistency but I thought it would be good to raise these points.
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.
@adelbertc I do not pay enough attention to have a definitive list. An obvious example would be the fact that all monads are stacksafe:
https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/FlatMap.scala#L102
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.
@jedws Ah there was quite a bit of talk on the location of that particular function during this past weekend at Scala by the Bay :-)
I'd like to avoid having such a discussion on this particular ticket, but if it's OK with you perhaps we can chat somewhere else about this? I'd be very interested in hearing your thoughts about it. I could ping you on IRC, or if you're on Gitter that works too.
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.
@jedws I agree we should not use this ticket to rehash old discussions, but I would be interested to hear from you if you can give examples of monads that can't be made stack safe.
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.
Going back to this line in particular, I would prefer not having this method, especially on something as abstract as Foldable
.
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.
@adelbertc I presume that you mean regardless of the name? Should getUnsafe
be removed from corresponding classes for the sake of consistency?
@@ -73,6 +73,8 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { | |||
|
|||
override def size[A](fa: Vector[A]): Long = fa.size.toLong | |||
|
|||
override def get[A](fa: Vector[A])(idx: Long): Option[A] = if (fa.size > idx && idx > 0) Some(fa(idx.toInt)) else None |
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.
Ahh, I see you have the Vector
here, but not yet List (which can be made faster than a full fold) or NonEmpties.
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.
Added for List
and NonEmpties
cc8e7c9
to
015b927
Compare
override def get[A](fa: List[A])(idx: Long): Option[A] = | ||
if (idx < 0 || fa.isEmpty) { | ||
None | ||
} else if (idx < 0) { |
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 should be idx > 0
right?
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.
Yes! Will change this.
@@ -216,6 +216,9 @@ private[data] sealed trait NonEmptyVectorInstances { | |||
override def foldRight[A, B](fa: NonEmptyVector[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = | |||
fa.foldRight(lb)(f) | |||
|
|||
override def get[A](fa: NonEmptyVector[A])(idx: Long): Option[A] = | |||
fa.get(idx.toInt) |
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.
we might want to check that idx
fits in a Int
else return None
, right?
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.
In general I wonder if it's worth just using Int
as the index. I can't think of many structures you can index into with Long
values that you'd also want to fold over. (But that doesn't mean they don't exist!)
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, I think Int
is more consistent with scala to be sure. Also, it is going to take quite a while to index past the 2 billionth element. If you need that, I doubt you are hindered by not having it in the typeclass directly.
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.
I used a Long
because the size
is a Long
. It seems strange that your collection can be a certain size
without allowing you to get elements close to the end.
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.
I don't think it makes sense to offer size as a long and get with an idx: Int
. 👍 to checking if it fits if an Int
and returning None
otherwise.
015b927
to
165da9b
Compare
@@ -70,6 +70,16 @@ trait ListInstances extends cats.kernel.instances.ListInstances { | |||
G.map2Eval(f(a), lglb)(_ :: _) | |||
}.value | |||
|
|||
@tailrec | |||
override def get[A](fa: List[A])(idx: Long): Option[A] = |
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.
can I request we avoid .head
etc... unless needed? Like:
fa match {
case Nil => None
case h :: tail =>
if (idx < 0) None
else if (idx == 0) h
else get(tail)(idx - 1)
}
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.
done.
@@ -73,6 +73,8 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { | |||
|
|||
override def size[A](fa: Vector[A]): Long = fa.size.toLong | |||
|
|||
override def get[A](fa: Vector[A])(idx: Long): Option[A] = if (idx < Int.MaxValue && fa.size > idx && idx >= 0) Some(fa(idx.toInt)) else None |
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.
can you put a \n
after =
to put this code under def get
?
@yilinwei Do you have time to address the code review comments? This is very useful. |
@edmundnoble Yes; I'll update/rebase this tomorrow. |
1863379
to
b500bf1
Compare
Hey, sorry for dragging this out; could you also override |
@edmundnoble I've done all except for Do you have a different implementation in mind? |
@yilinwei I thought about it some more, and override def get[A](fa: EitherK[F, G, A])(idx: Long): Option[A] =
fa.run.fold(F.get(_)(idx), G.get(_)(idx)) |
@edmundnoble I arrived at the same conclusion for |
The 2.10 build had timed out, but after a restart it worked fine. I suspect that it won't be an issue with split out of the JS builds that has recently happened on 👍 from me. Thanks @yilinwei! |
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.
Sorry for the very late feedback.
I left a few comments, but this looks really good. Thanks also for the added tests and cleanup in FoldableTests. 👍
@@ -117,6 +117,9 @@ private[data] sealed trait OneAndInstances extends OneAndLowPriority2 { | |||
new NonEmptyReducible[OneAnd[F, ?], F] { | |||
override def split[A](fa: OneAnd[F, A]): (A, F[A]) = (fa.head, fa.tail) | |||
|
|||
override def get[A](fa: OneAnd[F, A])(idx: Long): Option[A] = | |||
if (idx == 0L) Some(fa.head) else F.get(fa.tail)(idx - 1L) |
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.
I think we can add this implementation to NonEmptyReducible
(in Reducible.scala) as well,
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.
Done. I know there isn't a test currently; but I think that if such a test exists it should be comparing the default implementation to any overrides which exist.
Do you mind if we put that on a different issue because I think that the consistency of the methods is important.
@@ -404,6 +404,9 @@ private[data] sealed abstract class ValidatedInstances2 { | |||
override def size[A](fa: Validated[E, A]): Long = | |||
fa.fold(_ => 0L, _ => 1L) | |||
|
|||
override def get[A](fa: Validated[E, A])(idx: Long): Option[A] = | |||
if (idx == 0L) fa.fold(_ => None, Some(_)) else None |
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.
We can use Validated#toOption
.
case Failure(_) => None | ||
case Success(a) => | ||
if (idx == 0L) Some(a) else None | ||
} |
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.
Nitpicking: maybe like in Validated
:
if (idx == 0L) fa.toOption else None
def iterator[T](tuple: (Int, T)) = Some(tuple._2).iterator | ||
} | ||
|
||
class FoldableOneAndCheck extends FoldableCheck[OneAnd[List, ?]]("oneAnd") { |
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 is sort of already checked through ReducibleNonEmptyStreamCheck
in OneAndTests.scala
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.
I agree there is some duplication, but I don't think it should be tackled in this issue and should probably a different issue where we discuss what to do with tests for a typeclass which aren't part of the discipline laws.
|
||
import FoldableComposedCheck._ | ||
|
||
class FoldableComposedCheck extends FoldableCheck[λ[α => List[Option[α]]]]("composed") { |
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.
We can maybe check this with Nested[List, Option, ?]
, that way we don't need the FoldableComposedCheck
companion object. WDYT?
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.
👍 I thought this existed but couldn't remember the name of it.
override def size[B](fa: A Ior B): Long = fa.fold(_ => 0L, _ => 1L, (_, _) => 1L) | ||
|
||
override def get[B](fa: A Ior B)(idx: Long): Option[B] = | ||
if (idx == 0L) fa.fold(_ => None, Some(_), (_, b) => Some(b)) else None |
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.
We can use Ior#toOption
instead of the fold
.
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 the changes.
Merging with 3 👍, thanks @yilinwei. |
@@ -226,6 +226,9 @@ abstract class NonEmptyReducible[F[_], G[_]](implicit G: Foldable[G]) extends Re | |||
1 + G.size(tail) | |||
} | |||
|
|||
override def get[A](fa: F[A])(idx: Long): Option[A] = | |||
if (idx == 0L) Some(split(fa)._1) else G.get(split(fa)._2)(idx) |
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.
Shouldn't this be else G.get(split(fa)._2)(idx - 1L)
?
It's a bit funny that the only line of your PR that isn't covered by a unit test is the one with a bug in it 😜
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're right.
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.
Shit, looked over that one too quickly as well. Thanks @ceedubs for paying attention.
@peterneyens @yilinwei oops I forgot to hit the "submit review" button after making this review comment earlier :( |
No description provided.