Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/non/cats into monoidal-fu…
Browse files Browse the repository at this point in the history
…nctors
  • Loading branch information
julienrf committed Dec 10, 2015
2 parents 4b7d967 + 4f0cd09 commit e362fc9
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 46 deletions.
25 changes: 21 additions & 4 deletions CONTRIBUTING.md
Expand Up @@ -114,10 +114,27 @@ Write about https://github.com/non/cats/pull/36#issuecomment-72892359

### Write tests

Tests go into the tests module, under the `cats.tests` package. Cats tests
should extend `CatsSuite`. `CatsSuite` integrates ScalaTest with Discipline
for law checking, and imports all syntax and standard instances for
convenience.
- Tests for cats-core go into the tests module, under the `cats.tests` package.
- Tests for additional modules, such as `free`, go into the tests directory within that module.
- Cats tests should extend `CatsSuite`. `CatsSuite` integrates [ScalaTest](http://www.scalatest.org/)
with [Discipline](https://github.com/typelevel/discipline) for law checking, and imports all syntax and standard instances for convenience.
- The first parameter to the `checkAll` method provided by
[Discipline](https://github.com/typelevel/discipline), is the name of the test and will be output to the
console as part of the test execution. By convention:
- When checking laws, this parameter generally takes a form that describes the data type being tested.
For example the name *"Validated[String, Int]"* might be used when testing a type class instance
that the `Validated` data type supports.
- An exception to this is serializability tests, where the type class name is also included in the name.
For example, in the case of `Validated`, the serializability test would take the form,
*"Applicative[Validated[String, Int]"*, to indicate that this test is verifying that the `Applicative`
type class instance for the `Validated` data type is serializable.
- This convention helps to ensure clear and easy to understand output, with minimal duplication in the output.
- It is also a goal that, for every combination of data type and supported type class instance:
- Appropriate law checks for that combination are included to ensure that the instance meets the laws for that type class.
- A serializability test for that combination is also included, such that we know that frameworks which
rely heavily on serialization, such as `Spark`, will have strong compatibility with `cats`.
- Note that custom serialization tests are not required for instances of type classes which come from
`algebra`, such as `Monoid`, because the `algebra` laws include a test for serialization.

TODO

Expand Down
15 changes: 12 additions & 3 deletions build.sbt
Expand Up @@ -18,6 +18,10 @@ lazy val buildSettings = Seq(
crossScalaVersions := Seq("2.10.5", "2.11.7")
)

lazy val catsDoctestSettings = Seq(
doctestWithDependencies := false
) ++ doctestSettings

lazy val commonSettings = Seq(
scalacOptions ++= commonScalacOptions,
resolvers ++= Seq(
Expand All @@ -43,12 +47,16 @@ lazy val commonJsSettings = Seq(

lazy val commonJvmSettings = Seq(
testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF")
)
// currently sbt-doctest doesn't work in JS builds, so this has to go in the
// JVM settings. https://github.com/tkawachi/sbt-doctest/issues/52
) ++ catsDoctestSettings

lazy val catsSettings = buildSettings ++ commonSettings ++ publishSettings ++ scoverageSettings

lazy val scalacheckVersion = "1.12.5"

lazy val disciplineDependencies = Seq(
libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.12.5",
libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalacheckVersion,
libraryDependencies += "org.typelevel" %%% "discipline" % "0.4"
)

Expand Down Expand Up @@ -122,6 +130,7 @@ lazy val core = crossProject.crossType(CrossType.Pure)
.settings(
sourceGenerators in Compile <+= (sourceManaged in Compile).map(Boilerplate.gen)
)
.settings(libraryDependencies += "org.scalacheck" %%% "scalacheck" % scalacheckVersion % "test")
.jsSettings(commonJsSettings:_*)
.jvmSettings(commonJvmSettings:_*)

Expand Down Expand Up @@ -218,7 +227,7 @@ lazy val publishSettings = Seq(
) ++ credentialSettings ++ sharedPublishSettings ++ sharedReleaseProcess

// These aliases serialise the build for the benefit of Travis-CI.
addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;freeJVM/compile;freeJVM/test;stateJVM/compile;stateJVM/test;lawsJVM/compile;testsJVM/test;jvm/test;bench/test")
addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;coreJVM/test;freeJVM/compile;freeJVM/test;stateJVM/compile;stateJVM/test;lawsJVM/compile;testsJVM/test;jvm/test;bench/test")

addCommandAlias("validateJVM", ";scalastyle;buildJVM;makeSite")

Expand Down
30 changes: 20 additions & 10 deletions core/src/main/scala/cats/Foldable.scala
Expand Up @@ -87,10 +87,15 @@ import simulacrum.typeclass
* For example:
*
* {{{
* def parseInt(s: String): Option[Int] = ...
* val F = Foldable[List]
* F.traverse_(List("333", "444"))(parseInt) // Some(())
* F.traverse_(List("333", "zzz"))(parseInt) // None
* scala> import cats.data.Xor
* scala> import cats.std.list._
* scala> import cats.std.option._
* scala> def parseInt(s: String): Option[Int] = Xor.catchOnly[NumberFormatException](s.toInt).toOption
* scala> val F = Foldable[List]
* scala> F.traverse_(List("333", "444"))(parseInt)
* res0: Option[Unit] = Some(())
* scala> F.traverse_(List("333", "zzz"))(parseInt)
* res1: Option[Unit] = None
* }}}
*
* This method is primarily useful when `G[_]` represents an action
Expand All @@ -111,9 +116,13 @@ import simulacrum.typeclass
* For example:
*
* {{{
* val F = Foldable[List]
* F.sequence_(List(Option(1), Option(2), Option(3))) // Some(())
* F.sequence_(List(Option(1), None, Option(3))) // None
* scala> import cats.std.list._
* scala> import cats.std.option._
* scala> val F = Foldable[List]
* scala> F.sequence_(List(Option(1), Option(2), Option(3)))
* res0: Option[Unit] = Some(())
* scala> F.sequence_(List(Option(1), None, Option(3)))
* res1: Option[Unit] = None
* }}}
*/
def sequence_[G[_]: Applicative, A, B](fga: F[G[A]]): G[Unit] =
Expand All @@ -128,9 +137,10 @@ import simulacrum.typeclass
* For example:
*
* {{{
* val F = Foldable[List]
* F.foldK(List(1 :: 2 :: Nil, 3 :: 4 :: 5 :: Nil))
* // List(1, 2, 3, 4, 5)
* scala> import cats.std.list._
* scala> val F = Foldable[List]
* scala> F.foldK(List(1 :: 2 :: Nil, 3 :: 4 :: 5 :: Nil))
* res0: List[Int] = List(1, 2, 3, 4, 5)
* }}}
*/
def foldK[G[_], A](fga: F[G[A]])(implicit G: MonoidK[G]): G[A] =
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala/cats/data/OptionT.scala
Expand Up @@ -108,8 +108,10 @@ object OptionT extends OptionTInstances {
* Note: The return type is a FromOptionPartiallyApplied[F], which has an apply method
* on it, allowing you to call fromOption like this:
* {{{
* val t: Option[Int] = ...
* val x: OptionT[List, Int] = fromOption[List](t)
* scala> import cats.std.list._
* scala> val o: Option[Int] = Some(2)
* scala> OptionT.fromOption[List](o)
* res0: OptionT[List, Int] = OptionT(List(Some(2)))
* }}}
*
* The reason for the indirection is to emulate currying type parameters.
Expand Down
13 changes: 11 additions & 2 deletions core/src/main/scala/cats/data/Validated.scala
Expand Up @@ -133,6 +133,12 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable {
*/
def map[B](f: A => B): Validated[E,B] = bimap(identity, f)

/**
* Apply a function to an Invalid value, returning a new Invalid value.
* Or, if the original valid was Valid, return it.
*/
def leftMap[EE](f: E => EE): Validated[EE,A] = bimap(f, identity)

/**
* When Valid, apply the function, marking the result as valid
* inside the Applicative's context,
Expand Down Expand Up @@ -221,6 +227,7 @@ private[data] sealed abstract class ValidatedInstances extends ValidatedInstance
implicit def validatedBifunctor: Bifunctor[Validated] =
new Bifunctor[Validated] {
override def bimap[A, B, C, D](fab: Validated[A, B])(f: A => C, g: B => D): Validated[C, D] = fab.bimap(f, g)
override def leftMap[A, B, C](fab: Validated[A, B])(f: A => C): Validated[C, B] = fab.leftMap(f)
}

implicit def validatedInstances[E](implicit E: Semigroup[E]): Traverse[Validated[E, ?]] with Applicative[Validated[E, ?]] =
Expand Down Expand Up @@ -280,8 +287,10 @@ trait ValidatedFunctions {
* Evaluates the specified block, catching exceptions of the specified type and returning them on the invalid side of
* the resulting `Validated`. Uncaught exceptions are propagated.
*
* For example: {{{
* val result: Validated[NumberFormatException, Int] = catchOnly[NumberFormatException] { "foo".toInt }
* For example:
* {{{
* scala> Validated.catchOnly[NumberFormatException] { "foo".toInt }
* res0: Validated[NumberFormatException, Int] = Invalid(java.lang.NumberFormatException: For input string: "foo")
* }}}
*/
def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T] = new CatchOnlyPartiallyApplied[T]
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala/cats/data/Xor.scala
Expand Up @@ -222,8 +222,10 @@ trait XorFunctions {
* Evaluates the specified block, catching exceptions of the specified type and returning them on the left side of
* the resulting `Xor`. Uncaught exceptions are propagated.
*
* For example: {{{
* val result: NumberFormatException Xor Int = catchOnly[NumberFormatException] { "foo".toInt }
* For example:
* {{{
* scala> Xor.catchOnly[NumberFormatException] { "foo".toInt }
* res0: Xor[NumberFormatException, Int] = Left(java.lang.NumberFormatException: For input string: "foo")
* }}}
*/
def catchOnly[T >: Null <: Throwable]: CatchOnlyPartiallyApplied[T] =
Expand Down
21 changes: 13 additions & 8 deletions core/src/main/scala/cats/data/XorT.scala
Expand Up @@ -118,12 +118,15 @@ final case class XorT[F[_], A, B](value: F[A Xor B]) {
*
* Example:
* {{{
* val v1: Validated[NonEmptyList[Error], Int] = ...
* val v2: Validated[NonEmptyList[Error], Int] = ...
* val xort: XorT[Error, Int] = ...
*
* val result: XorT[NonEmptyList[Error], Int] =
* xort.withValidated { v3 => (v1 |@| v2 |@| v3.leftMap(NonEmptyList(_))) { case (i, j, k) => i + j + k } }
* scala> import cats.std.option._
* scala> import cats.std.list._
* scala> import cats.syntax.apply._
* scala> type Error = String
* scala> val v1: Validated[NonEmptyList[Error], Int] = Validated.Invalid(NonEmptyList("error 1"))
* scala> val v2: Validated[NonEmptyList[Error], Int] = Validated.Invalid(NonEmptyList("error 2"))
* scala> val xort: XorT[Option, Error, Int] = XorT(Some(Xor.left("error 3")))
* scala> xort.withValidated { v3 => (v1 |@| v2 |@| v3.leftMap(NonEmptyList(_))).map{ case (i, j, k) => i + j + k } }
* res0: XorT[Option, NonEmptyList[Error], Int] = XorT(Some(Left(OneAnd(error 1,List(error 2, error 3)))))
* }}}
*/
def withValidated[AA, BB](f: Validated[A, B] => Validated[AA, BB])(implicit F: Functor[F]): XorT[F, AA, BB] =
Expand All @@ -146,8 +149,10 @@ trait XorTFunctions {
* Note: The return type is a FromXorPartiallyApplied[F], which has an apply method
* on it, allowing you to call fromXor like this:
* {{{
* val t: Xor[String, Int] = ...
* val x: XorT[Option, String, Int] = fromXor[Option](t)
* scala> import cats.std.option._
* scala> val t: Xor[String, Int] = Xor.right(3)
* scala> XorT.fromXor[Option](t)
* res0: XorT[Option, String, Int] = XorT(Some(Right(3)))
* }}}
*
* The reason for the indirection is to emulate currying type parameters.
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/scala/cats/syntax/flatMap.scala
Expand Up @@ -34,7 +34,13 @@ final class FlatMapOps[F[_], A](fa: F[A])(implicit F: FlatMap[F]) {
* you can evaluate it only ''after'' the first action has finished:
*
* {{{
* fa.followedByEval(later(fb))
* scala> import cats.Eval
* scala> import cats.std.option._
* scala> import cats.syntax.flatMap._
* scala> val fa: Option[Int] = Some(3)
* scala> def fb: Option[String] = Some("foo")
* scala> fa.followedByEval(Eval.later(fb))
* res0: Option[String] = Some(foo)
* }}}
*/
def followedByEval[B](fb: Eval[F[B]]): F[B] = F.flatMap(fa)(_ => fb.value)
Expand Down
2 changes: 1 addition & 1 deletion free/src/test/scala/cats/free/FreeTests.scala
Expand Up @@ -76,7 +76,7 @@ sealed trait FreeTestsInstances {
}

implicit def freeArbitrary[F[_], A](implicit F: Arbitrary[F[A]], A: Arbitrary[A]): Arbitrary[Free[F, A]] =
Arbitrary(freeGen[F, A](6))
Arbitrary(freeGen[F, A](4))

implicit def freeEq[S[_]: Monad, A](implicit SA: Eq[S[A]]): Eq[Free[S, A]] =
new Eq[Free[S, A]] {
Expand Down
Expand Up @@ -26,7 +26,8 @@ trait MonadFilterTests[F[_]] extends MonadTests[F] {
name = "monadFilter",
parent = Some(monad[A, B, C]),
"monadFilter left empty" -> forAll(laws.monadFilterLeftEmpty[A, B] _),
"monadFilter right empty" -> forAll(laws.monadFilterRightEmpty[A, B] _))
"monadFilter right empty" -> forAll(laws.monadFilterRightEmpty[A, B] _),
"monadFilter consistency" -> forAll(laws.monadFilterConsistency[A, B] _))
}
}

Expand Down
3 changes: 2 additions & 1 deletion laws/src/main/scala/cats/laws/discipline/MonadTests.scala
Expand Up @@ -28,7 +28,8 @@ trait MonadTests[F[_]] extends ApplicativeTests[F] with FlatMapTests[F] {
def parents: Seq[RuleSet] = Seq(applicative[A, B, C], flatMap[A, B, C])
def props: Seq[(String, Prop)] = Seq(
"monad left identity" -> forAll(laws.monadLeftIdentity[A, B] _),
"monad right identity" -> forAll(laws.monadRightIdentity[A] _)
"monad right identity" -> forAll(laws.monadRightIdentity[A] _),
"map flatMap coherence" -> forAll(laws.mapFlatMapCoherence[A, B] _)
)
}
}
Expand Down
23 changes: 12 additions & 11 deletions project/plugins.sbt
Expand Up @@ -3,14 +3,15 @@ resolvers += Resolver.url(
url("http://dl.bintray.com/content/tpolecat/sbt-plugin-releases"))(
Resolver.ivyStylePatterns)

addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.2")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1")
addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0")
addSbtPlugin("pl.project13.scala"% "sbt-jmh" % "0.2.3")
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.4")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5")
addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.2")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.3")
addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.8.1")
addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.0")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.3")
addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.4")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5")
addSbtPlugin("com.github.tkawachi" % "sbt-doctest" % "0.3.5")
27 changes: 27 additions & 0 deletions tests/src/test/scala/cats/tests/OptionTests.scala
@@ -1,6 +1,7 @@
package cats
package tests

import cats.laws.{CoflatMapLaws, FlatMapLaws}
import cats.laws.discipline.{TraverseTests, CoflatMapTests, MonadCombineTests, SerializableTests, MonoidalTests}
import cats.laws.discipline.eq._

Expand All @@ -25,4 +26,30 @@ class OptionTests extends CatsSuite {
fs.show should === (fs.toString)
}
}

// The following two tests check the kleisliAssociativity and
// cokleisliAssociativity laws which are a different formulation of
// the flatMapAssociativity and coflatMapAssociativity laws. Since
// these laws are more or less duplicates of existing laws, we don't
// check them for all types that have FlatMap or CoflatMap instances.

test("Kleisli associativity") {
forAll { (l: Long,
f: Long => Option[Int],
g: Int => Option[Char],
h: Char => Option[String]) =>
val isEq = FlatMapLaws[Option].kleisliAssociativity(f, g, h, l)
isEq.lhs should === (isEq.rhs)
}
}

test("Cokleisli associativity") {
forAll { (l: Option[Long],
f: Option[Long] => Int,
g: Option[Int] => Char,
h: Option[Char] => String) =>
val isEq = CoflatMapLaws[Option].cokleisliAssociativity(f, g, h, l)
isEq.lhs should === (isEq.rhs)
}
}
}

0 comments on commit e362fc9

Please sign in to comment.