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

Port tests to Dotty #3552

Closed
wants to merge 14 commits into from
1 change: 1 addition & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ docstrings = JavaDoc
newlines.afterCurlyLambda = preserve
docstrings.style = Asterisk
docstrings.oneline = unfold
project.excludeFilters = [ "core/src/main/scala-3.x" ]
Copy link
Member Author

Choose a reason for hiding this comment

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

scalafmt can't deal with dotty syntax yet.

6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jdk:

scala_version_212: &scala_version_212 2.12.12
scala_version_213: &scala_version_213 2.13.3
dotty_version: &dotty_version 0.24.0
dotty_version: &dotty_version 0.27.0-RC1

before_install:
- export PATH=${PATH}:./vendor/bundle
Expand Down Expand Up @@ -53,7 +53,6 @@ jobs:
scala: *scala_version_213
after_success: codecov -F scala_version_213


- stage: test
name: Make Microsite on 2.12.10
env: TEST="docs"
Expand All @@ -75,11 +74,10 @@ jobs:
name: Binary compatibility 2.13
scala: *scala_version_213

# Note that we're currently only building some modules on Dotty, not running tests.
- stage: test
name: Dotty tests
env: TEST="Dotty tests"
script: sbt ++$TRAVIS_SCALA_VERSION! alleycatsLawsJVM/compile
script: sbt ++$TRAVIS_SCALA_VERSION! buildJVM bench/test
scala: *dotty_version

- stage: styling
Expand Down
5 changes: 1 addition & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,14 @@ lazy val includeGeneratedSrc: Setting[_] = {

lazy val disciplineDependencies = Seq(
libraryDependencies ++= Seq(
"org.scalacheck" %%% "scalacheck" % scalaCheckVersion,
"org.typelevel" %%% "discipline-core" % disciplineVersion
).map(_.withDottyCompat(scalaVersion.value))
)
)

lazy val testingDependencies = Seq(
libraryDependencies ++= Seq(
"org.scalameta" %%% "munit-scalacheck" % munitVersion % Test,
"org.typelevel" %%% "discipline-munit" % disciplineMunitVersion % Test
).map(
_.withDottyCompat(scalaVersion.value)
)
)

Expand Down
12 changes: 12 additions & 0 deletions core/src/main/scala-2.x/src/main/scala/cats/syntax/MonadOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cats.syntax

import cats.{Alternative, Monad}

final class MonadOps[F[_], A](private val fa: F[A]) extends AnyVal {
def whileM[G[_]](p: F[Boolean])(implicit M: Monad[F], G: Alternative[G]): F[G[A]] = M.whileM(p)(fa)
def whileM_(p: F[Boolean])(implicit M: Monad[F]): F[Unit] = M.whileM_(p)(fa)
def untilM[G[_]](p: F[Boolean])(implicit M: Monad[F], G: Alternative[G]): F[G[A]] = M.untilM(fa)(p)
def untilM_(p: F[Boolean])(implicit M: Monad[F]): F[Unit] = M.untilM_(fa)(p)
def iterateWhile(p: A => Boolean)(implicit M: Monad[F]): F[A] = M.iterateWhile(fa)(p)
def iterateUntil(p: A => Boolean)(implicit M: Monad[F]): F[A] = M.iterateUntil(fa)(p)
}
12 changes: 12 additions & 0 deletions core/src/main/scala-3.x/src/main/scala/cats/syntax/MonadOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cats.syntax

import cats.{Monad, Alternative}

final class MonadOps[F[_], A](private val fa: F[A]) extends AnyVal {
Copy link
Member Author

Choose a reason for hiding this comment

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

Had to split this out due to scala/scala3#9480

Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this flip the order of parameters though? That could be moderately source-incompatible. More importantly, it's inconsistent with the other syntax classes.

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually don't know how dotty compiles this down. Is explicitly still a thing? Do you have a suggestion on how to fix this given the limitation above?

Copy link
Member

Choose a reason for hiding this comment

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

I would summon @smarter actually if I hit this. Probably worth looking at the bytecode quickly to see what the parameter order is. Can we simply put the using block second?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's source-compatible (to explicitly pass parameters to a using block, you have to write using at use-site in front of the block too). I suggested putting the using block first because doing the implicit search requires instantiating type variables which in turns means that in increment.whileM_(inspect(i => i > 4)).run(3) we can figure out that i needs to be Int, see scala/scala3#9480 (comment) for the full explanation.

Copy link
Contributor

Choose a reason for hiding this comment

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

That would increase the maintenance burden though. But if you did want to rewrite all the ops classes for dotty, then you might as well use extension syntax which would look more natural:

extension [F[_], A](fa: F[A])(using M: Monad[F]) {
  def whileM_(p: F[Boolean]): F[Unit] = ???
  def untilM(...)
}

Copy link
Member

Choose a reason for hiding this comment

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

I'm just concerned that we're going to end up hitting something like this again in the future, but once we release on Dotty, we can't futz with the binary compatibility any further.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, you can break BC every time we break BC which is every six weeks until 3.0 is out probably, but afterwards it gets tricky yeah :) (though I guess you can always play trick with something like a package-protected overload to preserve BC?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Are we promising binary compatibility for dotty artifacts? Should we? (We certainly don't test it yet)

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we're promising BC until Scala 3 goes final. Once it does though, we 100% should be promising it.

def whileM[G[_]](using M: Monad[F], G: Alternative[G])(p: F[Boolean]): F[G[A]] = M.whileM(p)(fa)
def whileM_(using M: Monad[F])(p: F[Boolean]): F[Unit] = M.whileM_(p)(fa)
def untilM[G[_]](using M: Monad[F], G: Alternative[G])(p: F[Boolean]): F[G[A]] = M.untilM(fa)(p)
def untilM_(using M: Monad[F])(p: F[Boolean]): F[Unit] = M.untilM_(fa)(p)
def iterateWhile(using M: Monad[F])(p: A => Boolean): F[A] = M.iterateWhile(fa)(p)
def iterateUntil(using M: Monad[F])(p: A => Boolean): F[A] = M.iterateUntil(fa)(p)
}
13 changes: 10 additions & 3 deletions core/src/main/scala/cats/Invariant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ object Invariant extends ScalaVersionSpecificInvariantInstances with InvariantIn
implicit def catsFlatMapForSortedMap[K]: FlatMap[SortedMap[K, *]] =
cats.instances.sortedMap.catsStdInstancesForSortedMap[K]
implicit def catsBimonadForFunction0: Bimonad[Function0] = cats.instances.function.catsStdBimonadForFunction0
implicit def catsMonadForFunction1[I]: Monad[I => *] = cats.instances.function.catsStdMonadForFunction1[I]
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved this down because dotty thinks Monad[List] and Monad[Function1[Int, *]] are ambiguous

implicit def catsContravariantMonoidalForFunction1[R: Monoid]: ContravariantMonoidal[* => R] =
cats.instances.function.catsStdContravariantMonoidalForFunction1[R]
implicit def catsFunctorForPair: Functor[λ[P => (P, P)]] = cats.instances.tuple.catsDataFunctorForPair
Expand Down Expand Up @@ -285,19 +284,27 @@ object Invariant extends ScalaVersionSpecificInvariantInstances with InvariantIn

}

private[cats] trait InvariantInstances0 extends TupleInstances0 {
private[cats] trait InvariantInstances0 extends InvariantInstances1 {
implicit def catsCommutativeMonadForTuple2[X](implicit X: CommutativeMonoid[X]): CommutativeMonad[(X, *)] =
cats.instances.tuple.catsStdCommutativeMonadForTuple2[X]
implicit def catsContravariantForFunction1[R]: Contravariant[* => R] =
cats.instances.function.catsStdContravariantForFunction1[R]
implicit def catsDistributiveForFunction0: Distributive[Function0] = cats.instances.function.function0Distributive
implicit def catsDistributiveForFunction1[I]: Distributive[I => *] =
cats.instances.function.catsStdDistributiveForFunction1[I]

}

private trait InvariantInstances1 extends InvariantInstances2 {
implicit def catsMonadForFunction1[I]: Monad[I => *] = cats.instances.function.catsStdMonadForFunction1[I]
}

private[cats] trait InvariantInstances2 extends TupleInstances0 {
implicit def catsApplicativeForArrow[F[_, _], A](implicit F: Arrow[F]): Applicative[F[A, *]] =
Copy link
Member Author

Choose a reason for hiding this comment

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

Same with this one

new ArrowApplicative[F, A](F)
}

private trait TupleInstances0 extends TupleInstances1 {
private[cats] trait TupleInstances0 extends TupleInstances1 {
implicit def catsCommutativeFlatMapForTuple2[X](implicit X: CommutativeSemigroup[X]): CommutativeFlatMap[(X, *)] =
cats.instances.tuple.catsStdCommutativeFlatMapForTuple2[X]
}
Expand Down
9 changes: 0 additions & 9 deletions core/src/main/scala/cats/syntax/monad.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@ trait MonadSyntax {
new MonadIdOps[A](a)
}

final class MonadOps[F[_], A](private val fa: F[A]) extends AnyVal {
def whileM[G[_]](p: F[Boolean])(implicit M: Monad[F], G: Alternative[G]): F[G[A]] = M.whileM(p)(fa)
def whileM_(p: F[Boolean])(implicit M: Monad[F]): F[Unit] = M.whileM_(p)(fa)
def untilM[G[_]](p: F[Boolean])(implicit M: Monad[F], G: Alternative[G]): F[G[A]] = M.untilM(fa)(p)
def untilM_(p: F[Boolean])(implicit M: Monad[F]): F[Unit] = M.untilM_(fa)(p)
def iterateWhile(p: A => Boolean)(implicit M: Monad[F]): F[A] = M.iterateWhile(fa)(p)
def iterateUntil(p: A => Boolean)(implicit M: Monad[F]): F[A] = M.iterateUntil(fa)(p)
}

final class MonadIdOps[A](private val a: A) extends AnyVal {

/**
Expand Down
39 changes: 39 additions & 0 deletions free/src/main/scala-2.x/cats/free/FreeFoldStep.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cats.free

import Free.{FlatMapped, Pure, Suspend}
import cats.{Eval, Foldable}

private[free] trait FreeFoldStep[S[_], A] {

def step: Free[S, A]

/**
* A combination of step and fold. May be used to define interpreters with custom
* (non-monoidial) control flow.
*/
final def foldStep[B](
onPure: A => B,
onSuspend: S[A] => B,
onFlatMapped: ((S[X], X => Free[S, A]) forSome { type X }) => B
): B =
this.step match {
case Pure(a) => onPure(a)
case Suspend(a) => onSuspend(a)
case FlatMapped(Suspend(fa), f) => onFlatMapped((fa, f))
case _ => sys.error("FlatMapped should be right associative after step")
}

final def foldLeft[B](fa: Free[S, A], b: B)(f: (B, A) => B)(implicit F: Foldable[S]): B =
fa.foldStep(
a => f(b, a),
fa => F.foldLeft(fa, b)(f),
{ case (fx, g) => F.foldLeft(fx, b)((bb, x) => foldLeft(g(x), bb)(f)) }
)

final def foldRight[B](fa: Free[S, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B])(implicit F: Foldable[S]): Eval[B] =
fa.foldStep(
a => f(a, lb),
fa => F.foldRight(fa, lb)(f),
{ case (fx, g) => F.foldRight(fx, lb)((a, lbb) => foldRight(g(a), lbb)(f)) }
)
}
41 changes: 41 additions & 0 deletions free/src/main/scala-3.x/cats/free/FreeFoldStep.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cats.free

import Free.{FlatMapped, Pure, Suspend}
import cats.{Foldable, Eval}

private[free] trait FreeFoldStep[S[_], A] {

def step: Free[S, A]

private type OnFlatMapped[X] = (S[X], X => Free[S, A])

/**
* A combination of step and fold. May be used to define interpreters with custom
* (non-monoidial) control flow.
*/
final def foldStep[B](
onPure: A => B,
onSuspend: S[A] => B,
onFlatMapped: [X] => (S[X], X => Free[S, A]) => B
): B =
this.step match {
case Pure(a) => onPure(a)
case Suspend(a) => onSuspend(a)
case FlatMapped(Suspend(fa), f) => onFlatMapped(fa, f)
case _ => sys.error("FlatMapped should be right associative after step")
}

final def foldLeft[B](fa: Free[S, A], b: B)(f: (B, A) => B)(implicit F: Foldable[S]): B =
fa.foldStep(
a => f(b, a),
fa => F.foldLeft(fa, b)(f),
[X] => (sx: S[X], g: X => Free[S, A]) => F.foldLeft(sx, b)((bb, x) => foldLeft(g(x), bb)(f))
)

final def foldRight[B](fa: Free[S, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B])(implicit F: Foldable[S]): Eval[B] =
fa.foldStep(
a => f(a, lb),
fa => F.foldRight(fa, lb)(f),
[X] => (sx: S[X], g: X => Free[S, A]) => F.foldRight(sx, lb)((a, lbb) => foldRight(g(a), lbb)(f))
)
}
30 changes: 3 additions & 27 deletions free/src/main/scala/cats/free/Free.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import cats.arrow.FunctionK
* using the heap instead of the stack, allowing tail-call
* elimination.
*/
sealed abstract class Free[S[_], A] extends Product with Serializable {
sealed abstract class Free[S[_], A] extends Product with Serializable with FreeFoldStep[S, A] {

import Free.{FlatMapped, Pure, Suspend}

Expand Down Expand Up @@ -73,22 +73,6 @@ sealed abstract class Free[S[_], A] extends Product with Serializable {
}
}

/**
* A combination of step and fold. May be used to define interpreters with custom
* (non-monoidial) control flow.
*/
final def foldStep[B](
onPure: A => B,
onSuspend: S[A] => B,
onFlatMapped: ((S[X], X => Free[S, A]) forSome { type X }) => B
): B =
this.step match {
case Pure(a) => onPure(a)
case Suspend(a) => onSuspend(a)
case FlatMapped(Suspend(fa), f) => onFlatMapped((fa, f))
case _ => sys.error("FlatMapped should be right associative after step")
}

/**
* Run to completion, using a function that extracts the resumption
* from its suspension functor.
Expand Down Expand Up @@ -315,18 +299,10 @@ private trait FreeFoldable[F[_]] extends Foldable[Free[F, *]] {
implicit def F: Foldable[F]

final override def foldLeft[A, B](fa: Free[F, A], b: B)(f: (B, A) => B): B =
fa.foldStep(
a => f(b, a),
fa => F.foldLeft(fa, b)(f),
{ case (fx, g) => F.foldLeft(fx, b)((bb, x) => foldLeft(g(x), bb)(f)) }
)
fa.foldLeft(fa, b)(f)

final override def foldRight[A, B](fa: Free[F, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
fa.foldStep(
a => f(a, lb),
fa => F.foldRight(fa, lb)(f),
{ case (fx, g) => F.foldRight(fx, lb)((a, lbb) => foldRight(g(a), lbb)(f)) }
)
fa.foldRight(fa, lb)(f)
}

private trait FreeTraverse[F[_]] extends Traverse[Free[F, *]] with FreeFoldable[F] {
Expand Down
1 change: 1 addition & 0 deletions free/src/test/scala/cats/free/FreeApplicativeSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import cats.syntax.eq._
import org.scalacheck.Prop._
import cats.tests.CatsSuite
import org.scalacheck.{Arbitrary, Gen}
import cats._

class FreeApplicativeSuite extends CatsSuite {
import FreeApplicativeSuite._
Expand Down
2 changes: 1 addition & 1 deletion free/src/test/scala/cats/free/FreeSuite.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package cats.free

import cats.{:<:, Foldable, Functor, Id, Monad, Traverse}
import cats._
import cats.arrow.FunctionK
import cats.data.EitherK
import cats.instances.all._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import cats.syntax.parallel._
import cats.syntax.traverse._
import cats.syntax.eq._
import org.scalacheck.Prop._
import cats.catsInstancesForId
Copy link
Member Author

Choose a reason for hiding this comment

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

Id instances are no longer automatically in scope in dotty.


trait ScalaVersionSpecificFoldableSuite { self: FoldableSuiteAdditional =>
test("Foldable[LazyList].foldM stack safety") {
Expand Down
50 changes: 50 additions & 0 deletions tests/src/test/scala-2.x/cats/tests/FunctionKLiftSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cats.tests

import cats.data.NonEmptyList
import cats.arrow.FunctionK
import cats.implicits._
import org.scalacheck.Prop._
import cats.laws.discipline.arbitrary._

class FunctionKLiftSuite extends CatsSuite {

test("lift simple unary") {
def optionToList[A](option: Option[A]): List[A] = option.toList
val fOptionToList = FunctionK.lift(optionToList _)
forAll { (a: Option[Int]) =>
assert(fOptionToList(a) === (optionToList(a)))
}

val fO2I: FunctionK[Option, Iterable] = FunctionK.lift(Option.option2Iterable _)
forAll { (a: Option[String]) =>
assert(fO2I(a).toList === (Option.option2Iterable(a).toList))
}

val fNelFromListUnsafe = FunctionK.lift(NonEmptyList.fromListUnsafe _)
forAll { (a: NonEmptyList[Int]) =>
assert(fNelFromListUnsafe(a.toList) === (NonEmptyList.fromListUnsafe(a.toList)))
}
}

test("hygiene") {
trait FunctionK
def optionToList[A](option: Option[A]): List[A] = option.toList
val fOptionToList = cats.arrow.FunctionK.lift(optionToList _)
forAll { (a: Option[Int]) =>
assert(fOptionToList(a) === (optionToList(a)))
}
}

test("lift compound unary") {
val fNelFromList = FunctionK.lift[List, λ[α => Option[NonEmptyList[α]]]](NonEmptyList.fromList _)
forAll { (a: List[String]) =>
assert(fNelFromList(a) === (NonEmptyList.fromList(a)))
}
}

{ // lifting concrete types should fail to compile
def sample[A](option: Option[A]): List[A] = option.toList
assert(compileErrors("FunctionK.lift(sample[String])").nonEmpty)
assert(compileErrors("FunctionK.lift(sample[Nothing])").nonEmpty)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class ApplicativeErrorSuite extends CatsSuite {
assert(compileErrors("e2.attemptNarrow[Num]").nonEmpty)

val e3: Either[List[T[String]], Unit] = List(Str).asLeft[Unit]
assert(compileErrors("e3.attemptNarrow[List[Str.type]]").nonEmpty)
//assertEquals(compileErrors("e3.attemptNarrow[List[Str.type]]"), "")
Copy link
Member Author

Choose a reason for hiding this comment

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

This is an odd one, because when I try it in the console it gives me an error but munit returns an empty string here

}

test("attemptT syntax creates an EitherT") {
Expand Down
4 changes: 2 additions & 2 deletions tests/src/test/scala/cats/tests/ChainSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,13 @@ class ChainSuite extends CatsSuite {

test("== returns false for non-Chains") {
forAll { (a: Chain[Int], b: Int) =>
assert((a == b) === false)
assert((a.equals(b)) === false)
}
}

test("== returns false for Chains of different element types") {
forAll { (a: Chain[Option[String]], b: Chain[String]) =>
assert((a == b) === (a.isEmpty && b.isEmpty))
assert((a.equals(b)) === (a.isEmpty && b.isEmpty))
}
}

Expand Down
25 changes: 0 additions & 25 deletions tests/src/test/scala/cats/tests/ExtraRegressionSuite.scala

This file was deleted.

Loading