classDiagram
Contravariant~F[-_]~ <|-- Divide~F[-_]~
Divide <|-- Divisible~F[-_]~
Divide <|-- Decide~F[-_]~
Decide <|-- Decidable~F[-_]~
Divisible <|-- Decidable
class Contravariant {
// contravariant Functor
) contramap(F[A], B => A): F[B]
}
class Divide {
// contravariant Apply
) divide(A => (B,C), F[B], F[C]): F[A]
}
class Decide {
// contravariant Alt
) choose[A,B,C](A => Either[B, C], F[B], F[C]): F[A]
}
class Divisible {
// contravariant Applicative
) conquer[A]: F[A]
}
class Decidable {
// contravariant Alternative
) loose[A](A => Void): F[A]
}
Opposite category (nLab) is a category where all arrows are reversed.
case class Op[K[_,_],A,B](unOp: K[B,A])
or using type alias and specializing to category of Scala types and pure functions:
object Op {
type Op[A,B] = B => A
}
- Implementations: idris-ct Dual Category
Constructions in category C have dual constructions in category Op. Properties in given category holds also for dual construction in opposite category.
Examples of duality nLab
definition | dual (opposite) definition |
---|---|
initial object | terminal object |
product | coproduct |
limits | colimits |
monads | comonads |
flatmap | coflatmap |
functor | functor |
free | cofree |
monoid | comonoid |
end | coend |
initial F-algebra | terminal co-algebra |
Contravariance is different concept than duality.
Type constructor that can be contramap
, so map in opposite direction.
If we imagine Functor, as abstracting over some output, then Contravariant abstract over input.
In Category Theory Contravariant Functor is a Functor from opposite category (opposite category formalization in CubicalTT).
trait Contravariant[F[_]] {
def contramap[A, B](f: B => A): F[A] => F[B]
}
-
Implementations Scalaz 7, Scalaz 8, Cats, zio-prelude
-
Haskell, Purescript, UniMath, nLab Haskell, Purescript Proofs UniMath Category Theory nLab
flowchart RL
FA1(("F[A]")) --"contramap(id)"--> FA2(("F[A]"))
FA(("F[A]")) --"contramap(f andThen g)"--> FC(("F[C]"))
FA(("F[A]")) --"contramap(f)"--> FB(("F[B]"))--"contramap(g)"--> FC(("F[C]"))
1 contramap identity
// contramap(id)
// F[A] ================> F[A]
contramap(fa)(identity[A]) == fa
2 contravariant composition
// contramap f
// F[A] ==============> F[B]
val fb: F[B] = contramap(fa)(f)
// contramap g
// F[B] ===============> F[C]
val l: F[C] = contramap(fb)(g)
// contramap (g . f)
// F[A] =====================> F[B]
val r: F[C] = contramap(fa)(f compose g)
l == r
-
Applications:
- model function with fixed output type, like Predicate - function
A => Boolean
or Show. Edward Kmett used Contravariant functor hierarchy to model sorting. - Encoder in scodec has Contravariant instance: scodec/scodec-cats
- jberryman/simple-actors model Behavior of actor
- List of Haskell libraries using Contravariant functors
- Cats docs
- examples in scala_typeclassopedia
- model function with fixed output type, like Predicate - function
-
Contravariant is not called Cofunctor (like Monad -> Comonad, Appy -> Coapply) because when we inverse arrows in Functor definition, we just get Functor definition back (with A, B swapped). More on this on SO. There is library that makes fun ot of this fact acme-cofunctor.
-
Resources
- (Haskell) 24 Days of Hackage: contravariant - Tom Ellis (blog post) (all classic examples: Predicate, Op but alos Behaviors of actor, nice laws explanation)
- (Haskell) Covariance and Contravariance - Michael Snoyman blog post (Show as contravariant, discuss Profunctors, bivariance of phantom types, good explanation of positive and negative positions)
- (Haskell) I love profunctors. They're so easy. - Liyang HU (blog post) (Const, Predicate, Op instances for Contravariant)
- (Haskell) The Extended Functor Family - George Wilson (video)
- (Haskell) Contravariant Functors: The Other Side of the Coin - George Wilson (video)
- What is a contravariant functor? - SO
- What's up with Contravariant? - r/haskell
trait Divide[F[_]] extends Contravariant[F] {
def divide[A,B,C](f: A => (B,C), fb: F[B], fc: F[C]): F[A]
}
-
Implementations: Scalaz, Cats, Why not in Haskell, Purescript
-
Divide Laws (Scalaz, Haskell): let
def delta[A]: A => (A, A) = a => (a, a)
divide compositiondivide(divide(a1, a2)(delta), a3)(delta) == divide(a1, divide(a2, a3),(delta))(delta)
def divideComposition[A](fa1: F[A], fa2: F[A], fa3: F[A]): Boolean = {
// divide(delta)
// F[A1], F[A2] ===============> F[A12]
val fa12: F[A] = divide(delta[A], fa1, fa2)
// divide(delta)
// F[A12], F[A3] =================> F[A123]
val l: F[A] = divide( delta[A], fa12, fa3)
// divide(delta)
// F[A2], F[A3] ===============> F[A23]
val fa23: F[A] = divide(delta[A], fa2, fa3)
// divide(delta)
// F[A1], F[A23] ===============> F[A123]
val r: F[A] = divide( delta[A], fa1, fa23 )
l == r
}
This is simplified version. Proper version is shown in Haskell.
- Derived methods:
def divide1[A1, Z] (a1: F[A1]) (f: Z => A1): F[Z] // contramap
def divide2[A1, A2, Z](a1: F[A1], a2: F[A2])(f: Z => (A1, A2)): F[Z]
// ...
def tuple2[A1, A2] (a1: F[A1], a2: F[A2]): F[(A1, A2)]
def tuple3[A1, A2, A3](a1: F[A1], a2: F[A2], a3: F[A3]): F[(A1, A2, A3)]
// ...
def deriving2[A1, A2, Z](f: Z => (A1, A2))(implicit a1: F[A1], a2: F[A2]): F[Z]
def deriving3[A1, A2, A3, Z](f: Z => (A1, A2, A3))(implicit a1: F[A1], a2: F[A2], a3: F[A3]): F[Z]
// ...
- Resources
- (Haskell) Contravariant Functors: The Other Side of the Coin - George Wilson (video)
- (Haskell, Category Theory) Discrimination is Wrong: Improving Productivity - Edward Kmett (video) slides pdf
trait Divisible[F[_]] extends Divide[F] {
def conquer[A]: F[A]
}
-
Implementations: Scalaz 7, Cats Haskell, Purescript
-
Laws (Scalaz, Haskell): let
def delta[A]: A => (A, A) = a => (a, a)
-
all Contravariant and Divide laws including composition
divide(divide(a1, a2)(delta), a3)(delta) == divide(a1, divide(a2, a3),(delta))(delta)
(see Divide law) -
right identity:
divide(fa, conquer)(delta) == fa
def rightIdentity[A](fa: F[A]): Boolean = {
// divide(delta)
// F[A], conquer ===============> F[A]
val l: F[A] = divide(delta, fa, conquer[A])
l == fa
}
- left identity:
divide(conquer, fa)(delta) == fa
def leftIdentity[A](fa: F[A]): Boolean = {
// divide(delta)
// conquer, F[A] ===============> F[A]
val l: F[A] = divide(delta, conquer[A], fa)
l == fa
}
-
Instances: Predicate, Sorting, Serializable, Pritty Printing
-
Resources
- Cats PR #2034 explaining design choices different that in Haskell, Scalaz
- (Haskell) Contravariant Functors: The Other Side of the Coin - George Wilson (video)
- (Haskell, Category Theory) Discrimination is Wrong: Improving Productivity - Edward Kmett (video) slides pdf
- Implementations: Haskell
- Implementation: Haskell
Similar a Coyoneda yet, with existenctial function running in opposite direction.
trait ContravariantCoyoneda[F[_], A] {
type B
val fb: F[B]
val m: A => B
def lowerCoyoneda(implicit CF: Contravariant[F]): F[A] = CF.contramap(fb)(m) // run
}
def liftCoyoneda[F[_], AA](fa: F[AA]): ContravariantCoyoneda[F, AA] = new ContravariantCoyoneda[F, AA] {
type B = AA
val fb: F[B] = fa
val m: AA => B = identity[AA]
}
We can define Contravariant instance for Contravariant Coyoneda:
def cotraContraCoyo[F[_]] = new Contravariant[ContravariantCoyoneda[F, ?]] {
def contramap[AA, BB](fa: ContravariantCoyoneda[F, AA])(f: BB => AA): ContravariantCoyoneda[F, BB] = new ContravariantCoyoneda[F, BB] {
type B = fa.B
val fb: F[B] = fa.fb
val m: BB => B = fa.m compose f
}
}
-
Resources:
- Implementation: Haskell