Skip to content
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
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 @@ -45,10 +45,12 @@ package object cats {
type Order[A] = algebra.Order[A]
type Semigroup[A] = algebra.Semigroup[A]
type Monoid[A] = algebra.Monoid[A]
type Group[A] = algebra.Group[A]

val Eq = algebra.Eq
val PartialOrder = algebra.PartialOrder
val Order = algebra.Order
val Semigroup = algebra.Semigroup
val Monoid = algebra.Monoid
val Group = algebra.Group
}
2 changes: 2 additions & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ trait AllSyntax
with FlatMapSyntax
with FoldableSyntax
with FunctorSyntax
with GroupSyntax
with InvariantSyntax
with MonadCombineSyntax
with MonadFilterSyntax
with OrderSyntax
with PartialOrderSyntax
with ProfunctorSyntax
with ReducibleSyntax
with SemigroupSyntax
with SemigroupKSyntax
with Show.ToShowOps
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala/cats/syntax/eq.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ package syntax
import cats.macros.Ops

trait EqSyntax {
implicit def eqSyntax[A: Eq](a: A): EqOps[A] = new EqOps[A](a)
implicit def eqSyntax[A: Eq](a: A): EqOps[A] =
new EqOps[A](a)
}

class EqOps[A](lhs: A)(implicit A: Eq[A]) {
class EqOps[A: Eq](lhs: A) {
def ===(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def =!=(rhs: A): Boolean = macro Ops.binop[A, Boolean]
}
2 changes: 1 addition & 1 deletion core/src/main/scala/cats/syntax/foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ trait FoldableSyntax extends Foldable.ToFoldableOps with FoldableSyntax1 {

class NestedFoldableOps[F[_], G[_], A](fga: F[G[A]])(implicit F: Foldable[F]) {
def sequence_[B](implicit G: Applicative[G]): G[Unit] = F.sequence_(fga)
def foldK(fga: F[G[A]])(implicit G: MonoidK[G]): G[A] = F.foldK(fga)
def foldK(implicit G: MonoidK[G]): G[A] = F.foldK(fga)
}
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/syntax/group.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cats
package syntax

import cats.macros.Ops

trait GroupSyntax {
// TODO: use simulacrum instances eventually
implicit def groupSyntax[A: Group](a: A): GroupOps[A] =
new GroupOps[A](a)
}

class GroupOps[A: Group](lhs: A) {
def |-|(rhs: A): A = macro Ops.binop[A, A]
def remove(rhs: A): A = macro Ops.binop[A, A]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we be adding any unit tests along with this file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes! I'll add some law-checking code here. Good catch :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just a note -- the syntax tests I ended up writing found several omissions. So thanks for suggesting I add tests! 🐈

9 changes: 7 additions & 2 deletions core/src/main/scala/cats/syntax/order.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ package syntax
import cats.macros.Ops

trait OrderSyntax {
implicit def orderSyntax[A: Order](a: A): OrderOps[A] = new OrderOps[A](a)
implicit def orderSyntax[A: Order](a: A): OrderOps[A] =
new OrderOps[A](a)
}

class OrderOps[A](lhs: A)(implicit A: Order[A]) {
class OrderOps[A: Order](lhs: A) {
def <(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def <=(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def >(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def >=(rhs: A): Boolean = macro Ops.binop[A, Boolean]
def compare(rhs: A): Int = macro Ops.binop[A, Int]
def min(rhs: A): A = macro Ops.binop[A, A]
def max(rhs: A): A = macro Ops.binop[A, A]
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/cats/syntax/reducible.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ trait ReducibleSyntax extends Reducible.ToReducibleOps with ReducibleSyntax1 {
}

final class NestedReducibleOps[F[_], G[_], A](fga: F[G[A]])(implicit F: Reducible[F]) {
def reduceK(fga: F[G[A]])(implicit G: MonoidK[G]): G[A] = F.foldK(fga)
def reduceK(implicit G: SemigroupK[G]): G[A] = F.reduceK(fga)
}
10 changes: 6 additions & 4 deletions core/src/main/scala/cats/syntax/semigroup.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package cats
package syntax

import cats.macros.Ops

trait SemigroupSyntax {
// TODO: use simulacrum instances eventually
implicit def semigroupSyntax[A: Semigroup](a: A): SemigroupOps[A] =
new SemigroupOps[A](a)
}

class SemigroupOps[A](lhs: A)(implicit A: Semigroup[A]) {
def |+|(rhs: A): A = A.combine(lhs, rhs)
def combine(rhs: A): A = A.combine(lhs, rhs)
def combineN(rhs: Int): A = A.combineN(lhs, rhs)
class SemigroupOps[A: Semigroup](lhs: A) {
def |+|(rhs: A): A = macro Ops.binop[A, A]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I still don't have a good picture of what Cats is doing with Simulacrum and/or Machinist. What's the advantage of using macro Ops.binop[A, A] instead of the obvious A.combine(lhs, rhs)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Consider the following generic code using semigroup:

def sum[A: Semigroup](as: Iteratable[A]): A =
  as.foldLeft(0)(_ |+| _)

Say I pass it a Vector[Int] with 100,000 items. With the old code, I would instantiate 100k instances of SemigroupOps, one for each step in the foldLeft. With the new code, I would instantiate 0 instances of SemigroupOps; the code would be equivalent to:

def sum[A](as: Iteratable[A])(implicit ev: Semigroup[A]): A =
  as.foldLeft(0)((x, y) => ev.combine(x, y))

So it is an efficiency improvement that removes the penalty of using Ops implicits rather than explicitly threading and using the type class instances.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@non thanks. That makes sense. What's to prevent us from doing this in other places, such as in FlatMapOps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nothing -- I'd be happy to add this everywhere. I wanted to add it here since these are the places where the cost of the actual work (e.g. integer addition) is so small that the overhead would be particularly unfortunate (although we will still do better than Scalaz where we would be allocating a by-name parameter every time).

def combine(rhs: A): A = macro Ops.binop[A, A]
def combineN(rhs: Int): A = macro Ops.binop[A, A]
}
19 changes: 9 additions & 10 deletions tests/src/test/scala/cats/tests/CatsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package tests

import cats.std.AllInstances
import cats.syntax.AllSyntax
import org.scalatest.FunSuite
import org.scalatest.{ FunSuite, Matchers }
import org.typelevel.discipline.scalatest.Discipline

import org.scalacheck.{Arbitrary, Gen}
Expand All @@ -15,16 +15,15 @@ import scala.util.{Failure, Success, Try}
* An opinionated stack of traits to improve consistency and reduce
* boilerplate in Cats tests.
*/
trait CatsSuite extends FunSuite with Discipline with AllInstances with AllSyntax with TestInstances
trait CatsSuite extends FunSuite with Matchers with Discipline with AllInstances with AllSyntax with TestInstances {
// disable scalatest's ===
override def convertToEqualizer[T](left: T): Equalizer[T] = ???
}

sealed trait TestInstances {
// To be replaced by https://github.com/rickynils/scalacheck/pull/170
implicit def arbitraryTry[A : Arbitrary]: Arbitrary[Try[A]] =
Arbitrary {
for {
success <- arbitrary[Boolean]
t <- if (success) arbitrary[A].map(Success(_))
else Gen.const(Failure(new Throwable {}))
} yield t
}
implicit def arbitraryTry[A: Arbitrary]: Arbitrary[Try[A]] =
Arbitrary(Gen.oneOf(
arbitrary[A].map(Success(_)),
arbitrary[Throwable].map(Failure(_))))
}
3 changes: 0 additions & 3 deletions tests/src/test/scala/cats/tests/LazyTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import scala.math.min

class LazyTests extends CatsSuite {

// disable scalatest ===
override def convertToEqualizer[T](left: T): Equalizer[T] = ???

/**
* Class for spooky side-effects and action-at-a-distance.
*
Expand Down
181 changes: 181 additions & 0 deletions tests/src/test/scala/cats/tests/SyntaxTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package cats
package tests

import algebra.laws.GroupLaws
import cats.functor.{Invariant, Contravariant}
import cats.laws.discipline.SerializableTests

import org.scalacheck.{Arbitrary}
import org.scalatest.prop.PropertyChecks
import scala.reflect.runtime.universe.TypeTag

/**
* Test that our syntax implicits are working.
*
* Each method should correspond to one type class worth of syntax.
* Ideally, we should be testing every operator or method that we
* expect to add to generic parameters. This file is a safeguard
* against accidentally breaking (or removing) syntax which was
* otherwise untested.
*
* The strategy here is to create "mock" values of particular types,
* and then ensure that the syntax we want is available. We never plan
* to run any of these methods, so we don't need real values. All
* values in the methods should be generic -- we rely on parametricity
* to guarantee that the syntax will be available for any type with
* the proper type class instance(s).
*
* None of these tests should ever run, or do any runtime checks.
*/
class SyntaxTests extends CatsSuite with PropertyChecks {

// pretend we have a value of type A
def mock[A]: A = ???

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure if this is well known or not, but I wonder if using ??? here might become a problem one day given: https://github.com/tpolecat/tpolecat.github.io/blame/master/_posts/2014-04-11-scalac-flags.md#L21

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@dwijnand yeah I've run into that before as well. If it start causing trouble we can always switch it over to another hack :)


def testSemigroup[A: Semigroup]: Unit = {
val x = mock[A]
val y = mock[A]
val z: A = x |+| y
}

def testGroup[A: Group](x: A, y: A): Unit = {
val x = mock[A]
val y = mock[A]
val z: A = x |-| y
}

def testEq[A: Eq]: Unit = {
val x = mock[A]
val y = mock[A]
val b0: Boolean = x === y
val b1: Boolean = x =!= y
}

def testPartialOrder[A: PartialOrder]: Unit = {
val x = mock[A]
val y = mock[A]
val b0: Boolean = x < y
val b1: Boolean = x <= y
val b2: Boolean = x > y
val b3: Boolean = x >= y
val f: Double = x partialCompare y
val oi: Option[Int] = x tryCompare y
val oz0: Option[A] = x pmin y
val oz1: Option[A] = x pmax y
}

def testOrder[A: Order]: Unit = {
val x = mock[A]
val y = mock[A]
val i: Int = x compare y
val z0: A = x min y
val z1: A = x max y
}

def testInvariantFunctor[F[_]: Invariant, A, B]: Unit = {
val fa = mock[F[A]]
val f = mock[A => B]
val g = mock[B => A]
val fb: F[B] = fa.imap(f)(g)
}

def testInvariantFunctor[F[_]: Contravariant, A, B]: Unit = {
val fa = mock[F[A]]
val f = mock[B => A]
val fb: F[B] = fa.contramap(f)
}

def testFoldable[F[_]: Foldable, G[_]: Applicative: MonoidK, A: Monoid, B, Z]: Unit = {
val fa = mock[F[A]]
val b = mock[B]
val f1 = mock[(B, A) => B]
val b0: B = fa.foldLeft(b)(f1)
val a0: A = fa.fold

val f2 = mock[A => Fold[B]]
val lb0: Lazy[B] = fa.foldRight(Lazy(b))(f2)

val fz = mock[F[Z]]
val f3 = mock[Z => A]
val a1: A = fz.foldMap(f3)

val f4 = mock[A => G[B]]
val gu0: G[Unit] = fa.traverse_(f4)

val fga = mock[F[G[A]]]
val gu1: G[Unit] = fga.sequence_
val ga: G[A] = fga.foldK

val f5 = mock[A => Boolean]
val oa: Option[A] = fa.find(f5)

val as0: List[A] = fa.toList
val as1: List[A] = fa.filter_(f5)
val as2: List[A] = fa.dropWhile_(f5)
}

def testReducible[F[_]: Reducible, G[_]: Apply: SemigroupK, A: Semigroup, B, Z]: Unit = {
val fa = mock[F[A]]
val f1 = mock[(A, A) => A]
val a1: A = fa.reduceLeft(f1)

val f2 = mock[A => Fold[A]]
val la: Lazy[A] = fa.reduceRight(f2)

val a2: A = fa.reduce

val fga = mock[F[G[A]]]
val ga: G[A] = fga.reduceK

val fz = mock[F[Z]]
val f3 = mock[Z => A]
val a3: A = fz.reduceMap(f3)

val f4 = mock[A => B]
val f5 = mock[(B, A) => B]
val b1: B = fa.reduceLeftTo(f4)(f5)

val f6 = mock[A => Fold[B]]
val lb: Lazy[B] = fa.reduceRightTo(f4)(f6)

val f7 = mock[A => G[B]]
val gu1: G[Unit] = fa.traverse1_(f7)

val gu2: G[Unit] = fga.sequence1_
}

def testFunctor[F[_]: Functor, A, B]: Unit = {
val fa = mock[F[A]]
val f = mock[A => B]
val fb0: F[B] = fa.map(f)
val fu: F[Unit] = fa.void
val fab: F[(A, B)] = fa.fproduct(f)

val b = mock[B]
val fb1: F[B] = fa.as(b)
}

def testApply[F[_]: Apply, A, B, C, D, Z]: Unit = {
val fa = mock[F[A]]
val fab = mock[F[A => B]]
val fb0: F[B] = fa.ap(fab)

val fb = mock[F[B]]
val fabz = mock[F[(A, B) => Z]]
val fz0: F[Z] = fa.ap2(fb)(fabz)

val f = mock[(A, B) => Z]
val fz1: F[Z] = fa.map2(fb)(f)

val f1 = mock[(A, B) => Z]
val ff1 = mock[F[(A, B) => Z]]
val fz2: F[Z] = (fa |@| fb).map(f1)
val fz3: F[Z] = (fa |@| fb).ap(ff1)

val fc = mock[F[C]]
val f2 = mock[(A, B, C) => Z]
val ff2 = mock[F[(A, B, C) => Z]]
val fz4: F[Z] = (fa |@| fb |@| fc).map(f2)
val fz5: F[Z] = (fa |@| fb |@| fc).ap(ff2)
}
}