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

Enumerable (WIP) #4349

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,66 @@
package cats.kernel
package laws

trait EnumerableLaws[A] extends PartialNextLaws[A] with PartialPreviousLaws[A] {
implicit def En: Enumerable[A] // Not E to avoid conflict with
// PartialOrderLaws

// Note, we use integer numbers as a proxy for the natural numbers
// here. Since integer numbers have a bijective, e.g. one to one,
// correspondence with the natural numbers, and we have no good
// representation for a true Natural number in cats or the Scala stdlib.

def injectiveToNaturalNumbers(xs: Set[A]): IsEq[Int] =
xs.map(En.fromEnum).size <-> xs.size

def hasTheSameOrderAsBigInt(x: A, y: A): IsEq[Comparison] =
En.order.comparison(x, y) <-> Order[BigInt].comparison(En.fromEnum(x), En.fromEnum(y))

def bigIntHasTheSameOrderAsA(xi: BigInt, yi: BigInt): IsEq[Boolean] =
En.toEnumOpt(xi).flatMap(x =>
En.toEnumOpt(yi).map(y =>
Order[BigInt].comparison(xi, yi) == En.order.comparison(x, y)
)
).fold(
true <-> true
)(
_ <-> true
)
}

object EnumerableLaws {
def apply[A](implicit ev: Enumerable[A]): EnumerableLaws[A] =
new EnumerableLaws[A] {
override implicit val E: Order[A] = ev.order
override implicit val En: Enumerable[A] = ev
override implicit val N: PartialNext[A] = ev
override implicit val P: PartialPrevious[A] = ev
}
}

trait BoundlessEnumerableLaws[A] extends EnumerableLaws[A] {
implicit def En: BoundlessEnumerable[A] // Not E to avoid conflict with
// PartialOrderLaws

// Note, we use integer numbers as a proxy for the natural numbers
// here. Since integer numbers have a bijective, e.g. one to one,
// correspondence with the natural numbers, and we have no good
// representation for a true Natural number in cats or the Scala stdlib.

def bijectiveToNaturalNumbers(xs: Set[BigInt]): IsEq[Int] =
xs.map(En.toEnum).size <-> xs.size
}

object BoundlessEnumerableLaws {
def apply[A](implicit ev: BoundlessEnumerable[A]): BoundlessEnumerableLaws[A] =
new BoundlessEnumerableLaws[A] {
override implicit val E: Order[A] = ev.order
override implicit val En: BoundlessEnumerable[A] = ev
override implicit val N: PartialNext[A] = ev
override implicit val P: PartialPrevious[A] = ev
}
}

trait PartialPreviousLaws[A] extends PartialOrderLaws[A] {

implicit def P: PartialPrevious[A]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ import cats.kernel.instances.boolean._
import org.scalacheck.{Arbitrary, Prop}
import org.scalacheck.Prop.forAll

trait EnumerableTests[A] extends PartialNextTests[A] with PartialPreviousTests[A] {
def laws: EnumerableLaws[A]

def enumerable(implicit arbA: Arbitrary[A], arbF: Arbitrary[A => A], eqOA: Eq[Option[A]], eqA: Eq[A]): RuleSet =
new RuleSet {
override val name: String = "enumerable"
override val bases: Seq[(String, RuleSet)] = Nil
override val parents: Seq[RuleSet] = Seq(partialNext, partialPrevious, partialOrder)
override val props: Seq[(String, Prop)] = Seq(
"injective to the natural numbers" -> forAll(laws.injectiveToNaturalNumbers _)
)
}
}

object BoundlessEnumerableTests {
def apply[A: BoundlessEnumerable]: BoundlessEnumerableTests[A] =
new BoundlessEnumerableTests[A] { def laws: BoundlessEnumerableLaws[A] = BoundlessEnumerableLaws[A] }
}

trait BoundlessEnumerableTests[A] extends EnumerableTests[A] {
def laws: BoundlessEnumerableLaws[A]

def boundlessEnumerable(implicit arbA: Arbitrary[A], arbF: Arbitrary[A => A], eqOA: Eq[Option[A]], eqA: Eq[A]): RuleSet =
new RuleSet {
override val name: String = "boundlessEnumerable"
override val bases: Seq[(String, RuleSet)] = Nil
override val parents: Seq[RuleSet] = Seq(partialNext, partialPrevious, partialOrder, enumerable)
override val props: Seq[(String, Prop)] = Seq(
"bijective to the natural numbers" -> forAll(laws.bijectiveToNaturalNumbers _)
)
}
}

object EnumerableTests {
def apply[A: Enumerable]: EnumerableTests[A] =
new EnumerableTests[A] { def laws: EnumerableLaws[A] = EnumerableLaws[A] }
}

trait PartialNextTests[A] extends PartialOrderTests[A] {

def laws: PartialNextLaws[A]
Expand Down Expand Up @@ -56,6 +94,7 @@ trait PartialPreviousTests[A] extends PartialOrderTests[A] {

}

@deprecated(message = "Please use BoundableEnumerable and BoundableEnumerableTests", since = "2.10.0")
trait BoundedEnumerableTests[A] extends OrderTests[A] with PartialNextTests[A] with PartialPreviousTests[A] {

def laws: BoundedEnumerableLaws[A]
Expand All @@ -81,6 +120,7 @@ trait BoundedEnumerableTests[A] extends OrderTests[A] with PartialNextTests[A] w
}

object BoundedEnumerableTests {
@deprecated(message = "Please use BoundableEnumerable and BoundableEnumerableTests", since = "2.10.0")
def apply[A: BoundedEnumerable]: BoundedEnumerableTests[A] =
new BoundedEnumerableTests[A] { def laws: BoundedEnumerableLaws[A] = BoundedEnumerableLaws[A] }
}
27 changes: 16 additions & 11 deletions kernel-laws/shared/src/test/scala/cats/kernel/laws/LawTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Prop.forAll
import Arbitrary.arbitrary
import cats.kernel.instances.all.catsKernelStdOrderForDeadline

import scala.annotation.nowarn
import scala.concurrent.duration.{Deadline, Duration, FiniteDuration}
import scala.collection.immutable.{BitSet, Queue, SortedMap, SortedSet}
import scala.util.Random
Expand Down Expand Up @@ -211,22 +212,26 @@ class Tests extends TestsConfig with DisciplineSuite {
checkAll("UpperBounded[FiniteDuration]", UpperBoundedTests[FiniteDuration].upperBounded)
checkAll("UpperBounded[UUID]", UpperBoundedTests[UUID].upperBounded)

checkAll("BoundedEnumerable[Unit]", BoundedEnumerableTests[Unit].boundedEnumerable)
checkAll("BoundedEnumerable[Boolean]", BoundedEnumerableTests[Boolean].boundedEnumerable)
checkAll("BoundedEnumerable[Byte]", BoundedEnumerableTests[Byte].boundedEnumerable)
checkAll("BoundedEnumerable[Short]", BoundedEnumerableTests[Short].boundedEnumerable)
checkAll("BoundedEnumerable[Int]", BoundedEnumerableTests[Int].boundedEnumerable)
checkAll("BoundedEnumerable[Char]", BoundedEnumerableTests[Char].boundedEnumerable)
checkAll("BoundedEnumerable[Long]", BoundedEnumerableTests[Long].boundedEnumerable)
checkAll("BoundedEnumerable.reverse(BoundedEnumerable[Int])",
checkAll("Enumerable[Int]", EnumerableTests[Int].enumerable)

checkAll("BoundlessEnumerable[BigInt]", BoundlessEnumerableTests[BigInt].boundlessEnumerable)

(checkAll("BoundedEnumerable[Unit]", BoundedEnumerableTests[Unit].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable[Boolean]", BoundedEnumerableTests[Boolean].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable[Byte]", BoundedEnumerableTests[Byte].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable[Short]", BoundedEnumerableTests[Short].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable[Int]", BoundedEnumerableTests[Int].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable[Char]", BoundedEnumerableTests[Char].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable[Long]", BoundedEnumerableTests[Long].boundedEnumerable): @nowarn("msg=BoundableEnumerable"))
(checkAll("BoundedEnumerable.reverse(BoundedEnumerable[Int])",
BoundedEnumerableTests(BoundedEnumerable.reverse(BoundedEnumerable[Int])).boundedEnumerable
)
checkAll(
): @nowarn("msg=BoundableEnumerable"))
(checkAll(
"BoundedEnumerable.reverse(BoundedEnumerable.reverse(BoundedEnumerable[Int]))",
BoundedEnumerableTests(
BoundedEnumerable.reverse(BoundedEnumerable.reverse(BoundedEnumerable[Int]))
).boundedEnumerable
)
): @nowarn("msg=BoundableEnumerable"))

checkAll("Monoid[String]", MonoidTests[String].monoid)
checkAll("Monoid[String]", SerializableTests.serializable(Monoid[String]))
Expand Down
4 changes: 4 additions & 0 deletions kernel/src/main/scala-2.12/cats/kernel/EnumerableCompat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ package kernel
import scala.{specialized => sp}
import scala.collection.immutable.Stream

@deprecated(message = "Please use Enumerable instead.", since = "2.10.0")
trait PartialPreviousUpperBounded[@sp A] extends PartialPrevious[A] with PartialNext[A] with UpperBounded[A] {

/**
* Enumerate the members in descending order.
*/
@deprecated(message = "Please use Enumerable.membersDescending.", since = "2.10.0")
def membersDescending: Stream[A] = {
def loop(a: A): Stream[A] =
partialPrevious(a) match {
Expand All @@ -41,11 +43,13 @@ trait PartialPreviousUpperBounded[@sp A] extends PartialPrevious[A] with Partial

}

@deprecated(message = "Please use Enumerable instead.", since = "2.10.0")
trait PartialNextLowerBounded[@sp A] extends PartialPrevious[A] with PartialNext[A] with LowerBounded[A] {

/**
* Enumerate the members in ascending order.
*/
@deprecated(message = "Please use Enumerable.membersAscending.", since = "2.10.0")
def membersAscending: Stream[A] = {
def loop(a: A): Stream[A] =
partialNext(a) match {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cats
package kernel

private[kernel] object ScalaVersionSpecificLazyListCompat extends LazyListCompatBase {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder, can we simply define a type alias Stream => LazyList for Scala 2.12,
while keeping a usual LazyList for Scala 2.13+ ?

type LazyList[a] => Stream[a]
val LazyList = Stream

or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

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

We could probably do that, but we'd be implying that all methods used would be identical for Stream and LazyList. I think they are right now...and we could add exceptions if needed in the future...

So...yes? I'll try it out.

override final type T[A] = scala.collection.immutable.Stream[A]

override final def apply[A](a: A*): T[A] =
scala.collection.immutable.Stream.apply[A](a: _*)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ package kernel
import scala.{specialized => sp}
import scala.collection.immutable.LazyList

@deprecated(message = "Please use UpperBoundableEnumerable", since = "2.10.0")
trait PartialPreviousUpperBounded[@sp A] extends PartialPrevious[A] with PartialNext[A] with UpperBounded[A] {

/**
* Enumerate the members in descending order.
*/
@deprecated(message = "Please use UpperBoundableEnumerable.enumFromMax.", since = "2.10.0")
def membersDescending: LazyList[A] = {
def loop(a: A): LazyList[A] =
partialPrevious(a) match {
Expand All @@ -41,11 +43,13 @@ trait PartialPreviousUpperBounded[@sp A] extends PartialPrevious[A] with Partial

}

@deprecated(message = "Please use LowerBoundableEnumerable", since = "2.10.0")
trait PartialNextLowerBounded[@sp A] extends PartialPrevious[A] with PartialNext[A] with LowerBounded[A] {

/**
* Enumerate the members in ascending order.
*/
@deprecated(message = "Please use LowerBoundableEnumerable.enumFromMin.", since = "2.10.0")
def membersAscending: LazyList[A] = {
def loop(a: A): LazyList[A] =
partialNext(a) match {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cats
package kernel

private[kernel] object ScalaVersionSpecificLazyListCompat extends LazyListCompatBase {
override final type T[A] = scala.collection.immutable.LazyList[A]

override final def apply[A](a: A*): T[A] =
scala.collection.immutable.LazyList.apply[A](a: _*)
}
Loading