Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit functor variance #1097

Merged
merged 1 commit into from
Jun 9, 2016
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
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)
}
}
}