From 4c5db8b71a3a79e9ec7e4ce137d11185498820b1 Mon Sep 17 00:00:00 2001
From: Edmund Noble <edmundnoble@gmail.com>
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)
+    }
+  }
 }