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

Adding get for Foldable #1464

Merged
merged 6 commits into from
May 19, 2017
Merged

Conversation

yilinwei
Copy link
Contributor

No description provided.

@yilinwei yilinwei force-pushed the foldable-apply branch 7 times, most recently from e77602d to cc8e7c9 Compare November 13, 2016 22:24
@codecov-io
Copy link

codecov-io commented Nov 13, 2016

Codecov Report

Merging #1464 into master will increase coverage by 0.55%.
The diff coverage is 97.56%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
core/src/main/scala/cats/Reducible.scala 93.22% <0%> (-1.61%) ⬇️
core/src/main/scala/cats/instances/set.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/Foldable.scala 98.55% <100%> (+4.89%) ⬆️
core/src/main/scala/cats/data/Ior.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/either.scala 100% <100%> (+2.38%) ⬆️
core/src/main/scala/cats/data/NonEmptyVector.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/IdT.scala 70.83% <100%> (+2.65%) ⬆️
core/src/main/scala/cats/data/Validated.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/vector.scala 97.77% <100%> (+0.05%) ⬆️
core/src/main/scala/cats/instances/try.scala 96.36% <100%> (+0.06%) ⬆️
... and 32 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 586ea08...71d4fd6. Read the comment docs.

* 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] =
Copy link
Contributor

@edmundnoble edmundnoble Nov 14, 2016

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?

Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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

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

Copy link
Contributor Author

@yilinwei yilinwei Nov 14, 2016

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link

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

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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

override def get[A](fa: List[A])(idx: Long): Option[A] =
if (idx < 0 || fa.isEmpty) {
None
} else if (idx < 0) {
Copy link
Contributor

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?

Copy link
Contributor Author

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

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?

Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@@ -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] =
Copy link
Contributor

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

Copy link
Contributor Author

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

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?

@edmundnoble
Copy link
Contributor

@yilinwei Do you have time to address the code review comments? This is very useful.

@kailuowang kailuowang added this to the 1.0.0-MF milestone May 12, 2017
@yilinwei
Copy link
Contributor Author

@edmundnoble Yes; I'll update/rebase this tomorrow.

@yilinwei yilinwei force-pushed the foldable-apply branch 2 times, most recently from 1863379 to b500bf1 Compare May 13, 2017 22:52
@edmundnoble
Copy link
Contributor

edmundnoble commented May 14, 2017

Hey, sorry for dragging this out; could you also override get for all other Foldable/Traverse instances? Option, Either, Try, Id, IdT, Set, Map, EitherKTraverse, OneAnd, Validated, Const, Ior, Stream, TupleN, ComposedTraverse are all the ones I can find.

@yilinwei
Copy link
Contributor Author

yilinwei commented May 14, 2017

@edmundnoble I've done all except for EitherKTraverse and ComposedTraverse because I'd pretty much write the same thing as the current default implementation (minus maybe one call).

Do you have a different implementation in mind?

@edmundnoble
Copy link
Contributor

@yilinwei I thought about it some more, and ComposedTraverse is hard to override efficiently because to avoid making extra passes you have to get on the inner G and get the size of the G at once. EitherKTraverse's though looks like:

  override def get[A](fa: EitherK[F, G, A])(idx: Long): Option[A] =
    fa.run.fold(F.get(_)(idx), G.get(_)(idx))

@yilinwei
Copy link
Contributor Author

@edmundnoble I arrived at the same conclusion for Composed but I'm ashamed to say I temporarily forgot how subclassing worked for EitherK...

@ceedubs
Copy link
Contributor

ceedubs commented May 18, 2017

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

👍 from me. Thanks @yilinwei!

Copy link
Collaborator

@peterneyens peterneyens left a 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)
Copy link
Collaborator

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,

Copy link
Contributor Author

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

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
}
Copy link
Collaborator

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") {
Copy link
Collaborator

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

Copy link
Contributor Author

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") {
Copy link
Collaborator

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?

Copy link
Contributor Author

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

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.

@peterneyens peterneyens changed the title Adding apply and get for Foldable Adding get for Foldable May 19, 2017
Copy link
Collaborator

@peterneyens peterneyens left a 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.

@peterneyens
Copy link
Collaborator

Merging with 3 👍, thanks @yilinwei.

@peterneyens peterneyens merged commit cc4080c into typelevel:master May 19, 2017
@@ -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)
Copy link
Contributor

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 😜

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😢 You're right.

Copy link
Collaborator

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.

@ceedubs
Copy link
Contributor

ceedubs commented May 19, 2017

@peterneyens @yilinwei oops I forgot to hit the "submit review" button after making this review comment earlier :(

#1464 (comment)

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.

10 participants