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
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions core/src/main/scala/cats/Foldable.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cats

import scala.collection.mutable
import cats.instances.either._
import cats.instances.long._
import simulacrum.typeclass

Expand Down Expand Up @@ -151,6 +152,19 @@ import simulacrum.typeclass
*/
def size[A](fa: F[A]): Long = foldMap(fa)(_ => 1)

/**
* Get the element at the index of the `Foldable`.
*/
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. 👍

if (idx < 0L) None
else
foldM[Either[A, ?], A, Long](fa, 0L) { (i, a) =>
if (i == idx) Left(a) else Right(i + 1L)
} match {
case Left(a) => Some(a)
case Right(_) => None
}

/**
* Fold implemented using the given Monoid[A] instance.
*/
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/cats/data/Const.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ private[data] sealed abstract class ConstInstances extends ConstInstances0 {

def foldRight[A, B](fa: Const[C, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = lb

override def size[A](fa: Const[C, A]): Long = 0L

override def get[A](fa: Const[C, A])(idx: Long): Option[A] = None

override def traverse[G[_]: Applicative, A, B](fa: Const[C, A])(f: A => G[B]): G[Const[C, B]] =
fa.traverse(f)
}
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/IdT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ private[data] sealed trait IdTFoldable[F[_]] extends Foldable[IdT[F, ?]] {

def foldRight[A, B](fa: IdT[F, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
fa.foldRight(lb)(f)

override def size[A](fa: IdT[F, A]): Long =
F0.size(fa.value)

override def get[A](fa: IdT[F, A])(idx: Long): Option[A] =
F0.get(fa.value)(idx)
}

private[data] sealed trait IdTTraverse[F[_]] extends Traverse[IdT[F, ?]] with IdTFoldable[F] {
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/scala/cats/data/Ior.scala
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ private[data] sealed abstract class IorInstances0 {
def foldRight[B, C](fa: A Ior B, lc: Eval[C])(f: (B, Eval[C]) => Eval[C]): Eval[C] =
fa.foldRight(lc)(f)

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.


override def forall[B](fa: Ior[A, B])(p: (B) => Boolean): Boolean = fa.forall(p)

override def exists[B](fa: Ior[A, B])(p: (B) => Boolean): Boolean = fa.exists(p)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyList.scala
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,9 @@ private[data] sealed trait NonEmptyListInstances extends NonEmptyListInstances0
override def toList[A](fa: NonEmptyList[A]): List[A] = fa.toList

override def toNonEmptyList[A](fa: NonEmptyList[A]): NonEmptyList[A] = fa

override def get[A](fa: NonEmptyList[A])(idx: Long): Option[A] =
if (idx == 0) Some(fa.head) else Foldable[List].get(fa.tail)(idx - 1)
}

implicit def catsDataShowForNonEmptyList[A](implicit A: Show[A]): Show[NonEmptyList[A]] =
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,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] =
if (idx < Int.MaxValue) fa.get(idx.toInt) else None

def tailRecM[A, B](a: A)(f: A => NonEmptyVector[Either[A, B]]): NonEmptyVector[B] = {
val buf = new VectorBuilder[B]
@tailrec def go(v: NonEmptyVector[Either[A, B]]): Unit = v.head match {
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/OneAnd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.


override def size[A](fa: OneAnd[F, A]): Long = 1 + F.size(fa.tail)
}

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/Validated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.


override def foldMap[A, B](fa: Validated[E, A])(f: A => B)(implicit B: Monoid[B]): B =
fa.fold(_ => B.empty, f)

Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances {
override def size[B](fab: Either[A, B]): Long =
fab.fold(_ => 0L, _ => 1L)

override def get[B](fab: Either[A, B])(idx: Long): Option[B] =
if (idx == 0L) fab.fold(_ => None, Some(_)) else None

override def foldMap[B, C](fab: Either[A, B])(f: B => C)(implicit C: Monoid[C]): C =
fab.fold(_ => C.empty, f)

Expand Down
10 changes: 10 additions & 0 deletions core/src/main/scala/cats/instances/list.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.

fa match {
case Nil => None
case h :: tail =>
if (idx < 0) None
else if (idx == 0) Some(h)
else get(tail)(idx - 1)
}

override def exists[A](fa: List[A])(p: A => Boolean): Boolean =
fa.exists(p)

Expand Down
13 changes: 13 additions & 0 deletions core/src/main/scala/cats/instances/map.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ trait MapInstances extends cats.kernel.instances.MapInstances {

override def size[A](fa: Map[K, A]): Long = fa.size.toLong

override def get[A](fa: Map[K, A])(idx: Long): Option[A] = {
@tailrec
def go(idx: Int, it: Iterator[A]): Option[A] = {
if ( it.hasNext ) {
if (idx == 0) Some(it.next) else {
it.next
go(idx - 1, it)
}
} else None
}
if (idx > 0L && idx < fa.size && idx < Int.MaxValue) go(idx.toInt, fa.valuesIterator) else None
}

override def isEmpty[A](fa: Map[K, A]): Boolean = fa.isEmpty

override def foldM[G[_], A, B](fa: Map[K, A], z: B)(f: (B, A) => G[B])(implicit G: Monad[G]): G[B] =
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/option.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances {

override def maximumOption[A](fa: Option[A])(implicit A: Order[A]): Option[A] = fa

override def get[A](fa: Option[A])(idx: Long): Option[A] =
if (idx == 0L) fa else None

override def size[A](fa: Option[A]): Long = fa.fold(0L)(_ => 1L)

override def foldMap[A, B](fa: Option[A])(f: A => B)(implicit B: Monoid[B]): B =
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/instances/set.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cats
package instances

import scala.annotation.tailrec

import cats.syntax.show._

trait SetInstances extends cats.kernel.instances.SetInstances {
Expand All @@ -18,6 +20,19 @@ trait SetInstances extends cats.kernel.instances.SetInstances {
def foldRight[A, B](fa: Set[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
Foldable.iterateRight(fa.iterator, lb)(f)

override def get[A](fa: Set[A])(idx: Long): Option[A] = {
@tailrec
def go(idx: Int, it: Iterator[A]): Option[A] = {
if (it.hasNext) {
if (idx == 0) Some(it.next) else {
it.next
go(idx - 1, it)
}
} else None
}
if (idx < Int.MaxValue && idx >= 0L) go(idx.toInt, fa.toIterator) else None
}

override def size[A](fa: Set[A]): Long = fa.size.toLong

override def exists[A](fa: Set[A])(p: A => Boolean): Boolean =
Expand Down
11 changes: 11 additions & 0 deletions core/src/main/scala/cats/instances/stream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances {
override def forall[A](fa: Stream[A])(p: A => Boolean): Boolean =
fa.forall(p)

override def get[A](fa: Stream[A])(idx: Long): Option[A] = {
@tailrec
def go(idx: Long, s: Stream[A]): Option[A] =
s match {
case h #:: tail =>
if (idx == 0L) Some(h) else go(idx - 1L, tail)
case _ => None
}
if (idx < 0L) None else go(idx, fa)
}

override def isEmpty[A](fa: Stream[A]): Boolean = fa.isEmpty

override def filter[A](fa: Stream[A])(f: A => Boolean): Stream[A] = fa.filter(f)
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/instances/try.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ trait TryInstances extends TryInstances1 {
override def reduceRightOption[A](fa: Try[A])(f: (A, Eval[A]) => Eval[A]): Eval[Option[A]] =
Now(fa.toOption)

override def get[A](fa: Try[A])(idx: Long): Option[A] =
fa match {
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


override def size[A](fa: Try[A]): Long =
fa match {
case Failure(_) => 0L
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/tuple.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ sealed trait Tuple2Instances extends Tuple2Instances1 {

override def size[A](fa: (X, A)): Long = 1L

override def get[A](fa: (X, A))(idx: Long): Option[A] =
if (idx == 0L) Some(fa._2) else None

override def exists[A](fa: (X, A))(p: A => Boolean): Boolean = p(fa._2)

override def forall[A](fa: (X, A))(p: A => Boolean): Boolean = p(fa._2)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/vector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ 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

override def traverse[G[_], A, B](fa: Vector[A])(f: A => G[B])(implicit G: Applicative[G]): G[Vector[B]] =
foldRight[A, G[Vector[B]]](fa, Always(G.pure(Vector.empty))){ (a, lgvb) =>
G.map2Eval(f(a), lgvb)(_ +: _)
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/cats/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ package object cats {
Now(Some(f(fa)))
override def reduceMap[A, B](fa: Id[A])(f: A => B)(implicit B: Semigroup[B]): B = f(fa)
override def size[A](fa: Id[A]): Long = 1L
override def get[A](fa: Id[A])(idx: Long): Option[A] =
if (idx == 0L) Some(fa) else None
override def isEmpty[A](fa: Id[A]): Boolean = false
}

Expand Down
12 changes: 9 additions & 3 deletions tests/src/test/scala/cats/tests/FoldableTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ abstract class FoldableCheck[F[_]: Foldable](name: String)(implicit ArbFInt: Arb

def iterator[T](fa: F[T]): Iterator[T]

test(s"Foldable[$name].size") {
forAll { (fa: F[Int]) =>
fa.size should === (iterator(fa).size.toLong)
test(s"Foldable[$name].size/get") {
forAll { (fa: F[Int], n: Int) =>
val s = fa.size
s should === (iterator(fa).size.toLong)
if (n < s && n >= 0) {
fa.get(n.toLong) === Some(iterator(fa).take(n + 1).toList.last)
} else {
fa.get(n.toLong) === None
}
}
}

Expand Down