Skip to content

Commit

Permalink
Merge pull request #1097 from edmundnoble/functor-variance
Browse files Browse the repository at this point in the history
Explicit functor variance
  • Loading branch information
non committed Jun 9, 2016
2 parents 88cbe95 + 4c5db8b commit 8ec2caa
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 0 deletions.
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/Functor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/functor/Contravariant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docs/src/main/tut/contravariant.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`.
21 changes: 21 additions & 0 deletions docs/src/main/tut/functor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
21 changes: 21 additions & 0 deletions tests/src/test/scala/cats/tests/ContravariantTests.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
9 changes: 9 additions & 0 deletions tests/src/test/scala/cats/tests/FunctorTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

0 comments on commit 8ec2caa

Please sign in to comment.