From 4c5db8b71a3a79e9ec7e4ce137d11185498820b1 Mon Sep 17 00:00:00 2001 From: Edmund Noble Date: Fri, 18 Mar 2016 19:05:53 -0400 Subject: [PATCH] Explicit functor variance --- core/src/main/scala/cats/Functor.scala | 6 ++++++ .../scala/cats/functor/Contravariant.scala | 6 ++++++ docs/src/main/tut/contravariant.md | 18 ++++++++++++++++ docs/src/main/tut/functor.md | 21 +++++++++++++++++++ .../scala/cats/tests/ContravariantTests.scala | 21 +++++++++++++++++++ .../test/scala/cats/tests/FunctorTests.scala | 9 ++++++++ 6 files changed, 81 insertions(+) create mode 100644 tests/src/test/scala/cats/tests/ContravariantTests.scala diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index cf5183ed6d..1099181196 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -18,6 +18,12 @@ import simulacrum.typeclass // derived methods + /** + * Lifts natural subtyping covariance of covariant Functors. + * could be implemented as map(identity), but the Functor laws say this is equivalent + */ + def widen[A, B >: A](fa: F[A]): F[B] = fa.asInstanceOf[F[B]] + /** * Lift a function f to operate on Functors */ diff --git a/core/src/main/scala/cats/functor/Contravariant.scala b/core/src/main/scala/cats/functor/Contravariant.scala index cf231e38a2..d71af9ab24 100644 --- a/core/src/main/scala/cats/functor/Contravariant.scala +++ b/core/src/main/scala/cats/functor/Contravariant.scala @@ -16,6 +16,12 @@ import simulacrum.typeclass val G = Contravariant[G] } + /** + * Lifts natural subtyping contravariance of contravariant Functors. + * could be implemented as contramap(identity), but the Functor laws say this is equivalent + */ + def narrow[A, B <: A](fa: F[A]): F[B] = fa.asInstanceOf[F[B]] + override def composeFunctor[G[_]: Functor]: Contravariant[λ[α => F[G[α]]]] = new ComposedContravariantCovariant[F, G] { val F = self diff --git a/docs/src/main/tut/contravariant.md b/docs/src/main/tut/contravariant.md index df1dea5dc2..8762f2d800 100644 --- a/docs/src/main/tut/contravariant.md +++ b/docs/src/main/tut/contravariant.md @@ -28,6 +28,7 @@ Say we have class `Money` with a `Show` instance, and `Salary` class. ```tut:silent import cats._ +import cats.functor._ import cats.implicits._ case class Money(amount: Int) @@ -76,3 +77,20 @@ implicit val moneyOrdering: Ordering[Money] = Ordering.by(_.amount) Money(100) < Money(200) ``` +## Subtyping + +Contravariant functors have a natural relationship with subtyping, dual to that of covariant functors: + +```tut:book +class A +class B extends A +val b: B = new B +val a: A = b +val showA: Show[A] = Show.show(a => "a!") +val showB1: Show[B] = showA.contramap(b => b: A) +val showB2: Show[B] = showA.contramap(identity[A]) +val showB3: Show[B] = Contravariant[Show].narrow[A, B](showA) +``` + +Subtyping relationships are "lifted backwards" by contravariant functors, such that if `F` is a +lawful contravariant functor and `A <: B` then `F[B] <: F[A]`, which is expressed by `Contravariant.narrow`. diff --git a/docs/src/main/tut/functor.md b/docs/src/main/tut/functor.md index 98a8b7e5b8..89966b5348 100644 --- a/docs/src/main/tut/functor.md +++ b/docs/src/main/tut/functor.md @@ -119,3 +119,24 @@ Functor[Nested[List, Option, ?]].map(listOpt)(_ + 1) val optList = Nested[Option, List, Int](Some(List(1, 2, 3))) Functor[Nested[Option, List, ?]].map(optList)(_ + 1) ``` + +## Subtyping + +Functors have a natural relationship with subtyping: + +```tut:book +class A +class B extends A +val b: B = new B +val a: A = b +val listB: List[B] = List(new B) +val listA1: List[A] = listB.map(b => b: A) +val listA2: List[A] = listB.map(identity[A]) +val listA3: List[A] = Functor[List].widen[B, A](listB) +``` + +Subtyping relationships are "lifted" by functors, such that if `F` is a +lawful functor and `A <: B` then `F[A] <: F[B]` - almost. Almost, because to +convert an `F[B]` to an `F[A]` a call to `map(identity[A])` is needed +(provided as `widen` for convenience). The functor laws guarantee that +`fa map identity == fa`, however. diff --git a/tests/src/test/scala/cats/tests/ContravariantTests.scala b/tests/src/test/scala/cats/tests/ContravariantTests.scala new file mode 100644 index 0000000000..6e356c1b38 --- /dev/null +++ b/tests/src/test/scala/cats/tests/ContravariantTests.scala @@ -0,0 +1,21 @@ +package cats +package tests + +import cats.data.Const +import org.scalactic.CanEqual + +class ContravariantTest extends CatsSuite { + + test("narrow equals contramap(identity)") { + implicit val constInst = Const.catsDataContravariantForConst[Int] + implicit val canEqual: CanEqual[cats.data.Const[Int,Some[Int]],cats.data.Const[Int,Some[Int]]] = + StrictCatsEquality.lowPriorityConversionCheckedConstraint + forAll { (i: Int) => + val const: Const[Int, Option[Int]] = Const[Int, Option[Int]](i) + val narrowed: Const[Int, Some[Int]] = constInst.narrow[Option[Int], Some[Int]](const) + narrowed should === (constInst.contramap(const)(identity[Option[Int]](_: Some[Int]))) + assert(narrowed eq const) + } + } + +} diff --git a/tests/src/test/scala/cats/tests/FunctorTests.scala b/tests/src/test/scala/cats/tests/FunctorTests.scala index 9af4db22ae..b041cb3b9c 100644 --- a/tests/src/test/scala/cats/tests/FunctorTests.scala +++ b/tests/src/test/scala/cats/tests/FunctorTests.scala @@ -17,4 +17,13 @@ class FunctorTest extends CatsSuite { m.as(i) should === (m.keys.map(k => (k, i)).toMap) } } + + test("widen equals map(identity)") { + forAll { (i: Int) => + val list: List[Some[Int]] = List(Some(i)) + val widened: List[Option[Int]] = list.widen[Option[Int]] + widened should === (list.map(identity[Option[Int]])) + assert(widened eq list) + } + } }