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


override def fold[A](fa: F[A])(implicit A: Monoid[A]): A = {
val (a, ga) = split(fa)
A.combine(a, G.fold(ga))
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/EitherK.scala
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ private[data] trait EitherKFoldable[F[_], G[_]] extends Foldable[EitherK[F, G, ?
def foldLeft[A, B](fa: EitherK[F, G, A], z: B)(f: (B, A) => B): B =
fa.foldLeft(z)(f)

override def size[A](fa: EitherK[F, G, A]): Long =
fa.run.fold(F.size, G.size)

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

override def foldMap[A, B](fa: EitherK[F, G, A])(f: A => B)(implicit M: Monoid[B]): B =
fa foldMap 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.toOption else None

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.toOption else None

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
6 changes: 6 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,12 @@ 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] = {
if (idx >= 0L && idx < fa.size && idx < Int.MaxValue)
Some(fa.valuesIterator.drop(idx.toInt - 1).next)
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
3 changes: 3 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,9 @@ 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] =
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
51 changes: 46 additions & 5 deletions tests/src/test/scala/cats/tests/FoldableTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ import org.scalacheck.Arbitrary
import scala.util.Try

import cats.instances.all._
import cats.data.{NonEmptyList, NonEmptyStream, NonEmptyVector, Validated}
import cats.data._
import cats.laws.discipline.arbitrary._

abstract class FoldableCheck[F[_]: Foldable](name: String)(implicit ArbFInt: Arbitrary[F[Int]], ArbFString: Arbitrary[F[String]]) extends CatsSuite with PropertyChecks {

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 Expand Up @@ -217,7 +223,7 @@ class FoldableStreamCheck extends FoldableCheck[Stream]("stream") {
}

class FoldableMapCheck extends FoldableCheck[Map[Int, ?]]("map") {
def iterator[T](map: Map[Int, T]): Iterator[T] = map.iterator.map(_._2)
def iterator[T](map: Map[Int, T]): Iterator[T] = map.valuesIterator
}

class FoldableOptionCheck extends FoldableCheck[Option]("option") {
Expand All @@ -235,3 +241,38 @@ class FoldableValidatedCheck extends FoldableCheck[Validated[String, ?]]("valida
class FoldableTryCheck extends FoldableCheck[Try]("try") {
def iterator[T](tryt: Try[T]): Iterator[T] = tryt.toOption.iterator
}

class FoldableEitherKCheck extends FoldableCheck[EitherK[Option, Option, ?]]("eitherK") {
def iterator[T](eitherK: EitherK[Option, Option, T]) = eitherK.run.bimap(_.iterator, _.iterator).merge
}

class FoldableIorCheck extends FoldableCheck[Int Ior ?]("ior") {
def iterator[T](ior: Int Ior T) =
ior.fold(_ => None.iterator, b => Some(b).iterator, (_, b) => Some(b).iterator)
}

class FoldableIdCheck extends FoldableCheck[Id[?]]("id") {
def iterator[T](id: Id[T]) = Some(id).iterator
}

class FoldableIdTCheck extends FoldableCheck[IdT[Option, ?]]("idT") {
def iterator[T](idT: IdT[Option, T]) = idT.value.iterator
}

class FoldableConstCheck extends FoldableCheck[Const[Int, ?]]("const") {
def iterator[T](const: Const[Int, T]) = None.iterator
}

class FoldableTuple2Check extends FoldableCheck[(Int, ?)]("tuple2") {
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.

def iterator[T](oneAnd: OneAnd[List, T]) = (oneAnd.head :: oneAnd.tail).iterator
}

class FoldableComposedCheck extends FoldableCheck[Nested[List, Option, ?]]("nested") {
def iterator[T](nested: Nested[List, Option, T]) = nested.value.collect {
case Some(t) => t
}.iterator
}