Initial content: covariance and contravariance
This commit is contained in:
parent
b5f33fdc77
commit
b4e5af7276
2 changed files with 514 additions and 0 deletions
513
content/covariance-contravariance.md
Normal file
513
content/covariance-contravariance.md
Normal file
|
@ -0,0 +1,513 @@
|
|||
---
|
||||
title: Covariance, contravariance, and positive and negative position
|
||||
author: Michael Snoyman <michael@snoyman.com>
|
||||
description: Some common terms from category theory that are likely unfamiliar to those with an engineering background.
|
||||
first-written: 2015-10-20
|
||||
last-updated: 2015-10-20
|
||||
last-reviewed: 2015-10-20
|
||||
---
|
||||
|
||||
__NOTE__ This article is still awaiting review for correctness.
|
||||
|
||||
Typeclasses such as
|
||||
[Bifunctor](http://haddock.stackage.org/lts-3.10/base-4.8.1.0/Data-Bifunctor.html)
|
||||
are often expressed in terms of whether they are *covariant* or
|
||||
*contravariant*. While these terms may appear intimidating to the unfamiliar,
|
||||
they are a precise language for discussing these concepts, and once explained
|
||||
are relatively easy to understand. Furthermore, the related topics of *positive
|
||||
and negative position* can greatly simplify how you think about complex data
|
||||
structures. This topic also naturally leads into *subtyping*.
|
||||
|
||||
This article is intended to give a developer-focused explanation of the terms
|
||||
without diving into the category theory behind them too much. For more
|
||||
information, please see [the Wikipedia page on covariance and
|
||||
contravariance](https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29).
|
||||
|
||||
## Functors are covariant
|
||||
|
||||
Let's consider the following functions (made monomorphic for clarity):
|
||||
|
||||
```haskell
|
||||
showInt :: Int -> String
|
||||
showInt = show
|
||||
|
||||
floorInt :: Double -> Int
|
||||
floorInt = floor
|
||||
```
|
||||
|
||||
Now suppose that we have a value:
|
||||
|
||||
```haskell
|
||||
maybeInt :: Maybe Int
|
||||
maybeInt = Just 5
|
||||
```
|
||||
|
||||
We know `Maybe` is an instance of `Functor`, providing us with the following function:
|
||||
|
||||
```haskell
|
||||
fmapMaybe :: (a -> b) -> Maybe a -> Maybe b
|
||||
fmapMaybe = fmap
|
||||
```
|
||||
|
||||
We can use `fmapMaybe` and `showInt` together to get a new, valid, well-typed value:
|
||||
|
||||
```haskell
|
||||
maybeString :: Maybe String
|
||||
maybeString = fmapMaybe showInt maybeInt
|
||||
```
|
||||
|
||||
However, we can't do the same thing with `floorInt`. The reason for this is
|
||||
relatively straightforward: in order to use `fmapMaybe` on our `Maybe Int`, we
|
||||
need to provide a function that takes an `Int` as an input, whereas `floorInt`
|
||||
returns an `Int` as an output. This is a long-winded way of saying that `Maybe`
|
||||
is covariant on its type argument, or that the `Functor` typeclass is a
|
||||
covariant functor.
|
||||
|
||||
Doesn't make sense yet? Don't worry, it shouldn't. In order to understand this
|
||||
better, let's contrast it with something different.
|
||||
|
||||
## A non-covariant data type
|
||||
|
||||
Consider the following data structure representing how to create a `String`
|
||||
from something:
|
||||
|
||||
```haskell
|
||||
newtype MakeString a = MakeString { makeString :: a -> String }
|
||||
```
|
||||
|
||||
We can use this to convert an `Int` into a `String`:
|
||||
|
||||
```haskell
|
||||
newtype MakeString a = MakeString { makeString :: a -> String }
|
||||
|
||||
showInt :: MakeString Int
|
||||
showInt = MakeString show
|
||||
|
||||
main :: IO ()
|
||||
main = putStrLn $ makeString showInt 5
|
||||
```
|
||||
|
||||
The output for this program is, as expected, `5`. But suppose we want to both
|
||||
add `3` to the `Int` and turn it into a `String`. We can do:
|
||||
|
||||
```haskell
|
||||
newtype MakeString a = MakeString { makeString :: a -> String }
|
||||
|
||||
plus3ShowInt :: MakeString Int
|
||||
plus3ShowInt = MakeString (show . (+ 3))
|
||||
|
||||
main :: IO ()
|
||||
main = putStrLn $ makeString plus3ShowInt 5
|
||||
```
|
||||
|
||||
But this approach is quite non-compositional. We'd ideally like to be able to
|
||||
just apply more functions to this data structure. Let's first write that up
|
||||
without any typeclasses:
|
||||
|
||||
```haskell
|
||||
newtype MakeString a = MakeString { makeString :: a -> String }
|
||||
|
||||
mapMakeString :: (b -> a) -> MakeString a -> MakeString b
|
||||
mapMakeString f (MakeString g) = MakeString (g . f)
|
||||
|
||||
showInt :: MakeString Int
|
||||
showInt = MakeString show
|
||||
|
||||
plus3ShowInt :: MakeString Int
|
||||
plus3ShowInt = mapMakeString (+ 3) showInt
|
||||
|
||||
main :: IO ()
|
||||
main = putStrLn $ makeString plus3ShowInt 5
|
||||
```
|
||||
|
||||
But this kind of mapping inside a data structure is exactly what we use the
|
||||
`Functor` type class for, right? So let's try to write an instance!
|
||||
|
||||
```haskell
|
||||
instance Functor MakeString where
|
||||
fmap f (MakeString g) = MakeString (g . f)
|
||||
```
|
||||
|
||||
Unfortunately, this doesn't work:
|
||||
|
||||
```
|
||||
Main.hs:4:45:
|
||||
Couldn't match type ‘b’ with ‘a’
|
||||
‘b’ is a rigid type variable bound by
|
||||
the type signature for
|
||||
fmap :: (a -> b) -> MakeString a -> MakeString b
|
||||
at Main.hs:4:5
|
||||
‘a’ is a rigid type variable bound by
|
||||
the type signature for
|
||||
fmap :: (a -> b) -> MakeString a -> MakeString b
|
||||
at Main.hs:4:5
|
||||
Expected type: b -> a
|
||||
Actual type: a -> b
|
||||
Relevant bindings include
|
||||
g :: a -> String (bound at Main.hs:4:24)
|
||||
f :: a -> b (bound at Main.hs:4:10)
|
||||
fmap :: (a -> b) -> MakeString a -> MakeString b
|
||||
(bound at Main.hs:4:5)
|
||||
In the second argument of ‘(.)’, namely ‘f’
|
||||
In the first argument of ‘MakeString’, namely ‘(g . f)’
|
||||
```
|
||||
|
||||
To understand why, let's compare the type for `fmap` (specialized to
|
||||
`MakeString`) with our `mapMakeString` type:
|
||||
|
||||
```haskell
|
||||
mapMakeString :: (b -> a) -> MakeString a -> MakeString b
|
||||
fmap :: (a -> b) -> MakeString a -> MakeString b
|
||||
```
|
||||
|
||||
Notice that `fmap` has the usual `a -> b` parameter, whereas `mapMakeString`
|
||||
instead has a `b -> a`, which goes in the opposite direction. More on that
|
||||
next.
|
||||
|
||||
__Exercise__: Convince yourself that the `mapMakeString` function has the only
|
||||
valid type signature we could apply to it, and that the implementation is the
|
||||
only valid implementation of that signature. (It's true that you can change the
|
||||
variable names around to cheat and make the first parameter `a -> b`, but then
|
||||
you'd also have to modify the rest of the type signature.)
|
||||
|
||||
## Contravariance
|
||||
|
||||
What we just saw is that `fmap` takes a function from `a -> b`, and lifts it to
|
||||
`f a -> f b`. Notice that the `a` is always the "input" in both cases, whereas
|
||||
the `b` is the "output" in both cases. By contrast, `mapMakeString` has the
|
||||
normal `f a -> f b`, but the initial function has its types reversed: `b -> a`.
|
||||
This is the core of covariance vs contravariance:
|
||||
|
||||
* In covariance, both the original and lifted functions point in the same
|
||||
direction (from `a` to `b`)
|
||||
* In contravariance, the original and lifted functions point in *opposite*
|
||||
directions (one goes from `a` to `b`, the other from `b` to `a`)
|
||||
|
||||
This is what is meant when we refer to the normal `Functor` typeclass in
|
||||
Haskell as a covariant functor. And as you can probably guess, we can just as
|
||||
easily define a contravariant functor. In fact, [it exists in the contravariant
|
||||
package](http://haddock.stackage.org/lts-3.10/contravariant-1.3.3/Data-Functor-Contravariant.html#t:Contravariant).
|
||||
Let's go ahead and use that typeclass in our toy example:
|
||||
|
||||
```haskell
|
||||
import Data.Functor.Contravariant
|
||||
|
||||
newtype MakeString a = MakeString { makeString :: a -> String }
|
||||
|
||||
instance Contravariant MakeString where
|
||||
contramap f (MakeString g) = MakeString (g . f)
|
||||
|
||||
showInt :: MakeString Int
|
||||
showInt = MakeString show
|
||||
|
||||
plus3ShowInt :: MakeString Int
|
||||
plus3ShowInt = contramap (+ 3) showInt
|
||||
|
||||
main :: IO ()
|
||||
main = putStrLn $ makeString plus3ShowInt 5
|
||||
```
|
||||
|
||||
Our implementation of `contramap` is identical to the `mapMakeString` used
|
||||
before, which hopefully isn't too surprising.
|
||||
|
||||
### Example: filtering with `Predicate`
|
||||
|
||||
Let's say we want to print out all of the numbers from 1 to 10, where the
|
||||
English word for that number is more than three characters long. Using a simple
|
||||
helper function `english :: Int -> String` and `filter, this is pretty simple:
|
||||
|
||||
```
|
||||
greaterThanThree :: Int -> Bool
|
||||
greaterThanThree = (> 3)
|
||||
|
||||
lengthGTThree :: [a] -> Bool
|
||||
lengthGTThree = greaterThanThree . length
|
||||
|
||||
englishGTThree :: Int -> Bool
|
||||
englishGTThree = lengthGTThree . english
|
||||
|
||||
english :: Int -> String
|
||||
english 1 = "one"
|
||||
english 2 = "two"
|
||||
english 3 = "three"
|
||||
english 4 = "four"
|
||||
english 5 = "five"
|
||||
english 6 = "six"
|
||||
english 7 = "seven"
|
||||
english 8 = "eight"
|
||||
english 9 = "nine"
|
||||
english 10 = "ten"
|
||||
|
||||
main :: IO ()
|
||||
main = print $ filter englishGTThree [1..10]
|
||||
```
|
||||
|
||||
The contravariant package provides a newtype wrapper around such `a -> Bool`
|
||||
functions, called `Predicate`. We can use this newtype to wrap up our helper
|
||||
functions and avoid explicit function composition:
|
||||
|
||||
```haskell
|
||||
import Data.Functor.Contravariant
|
||||
|
||||
greaterThanThree :: Predicate Int
|
||||
greaterThanThree = Predicate (> 3)
|
||||
|
||||
lengthGTThree :: Predicate [a]
|
||||
lengthGTThree = contramap length greaterThanThree
|
||||
|
||||
englishGTThree :: Predicate Int
|
||||
englishGTThree = contramap english lengthGTThree
|
||||
|
||||
english :: Int -> String
|
||||
english 1 = "one"
|
||||
english 2 = "two"
|
||||
english 3 = "three"
|
||||
english 4 = "four"
|
||||
english 5 = "five"
|
||||
english 6 = "six"
|
||||
english 7 = "seven"
|
||||
english 8 = "eight"
|
||||
english 9 = "nine"
|
||||
english 10 = "ten"
|
||||
|
||||
main :: IO ()
|
||||
main = print $ filter (getPredicate englishGTThree) [1..10]
|
||||
```
|
||||
|
||||
__NOTE__: I'm not actually recommending this as a better practice than the
|
||||
original, simpler version. This is just to demonstrate the capability of the
|
||||
abstraction.
|
||||
|
||||
## Bifunctor and Profunctor
|
||||
|
||||
We're now ready to look at something a bit more complicated. Consider the
|
||||
following two typeclasses:
|
||||
[Profunctor](http://haddock.stackage.org/lts-3.10/profunctors-5.1.1/Data-Profunctor.html)
|
||||
and
|
||||
[Bifunctor](http://haddock.stackage.org/lts-3.10/base-4.8.1.0/Data-Bifunctor.html).
|
||||
Both of these typeclasses apply to types of kind `* -> * -> *`, also known as
|
||||
"a type constructor that takes two arguments." But let's look at their
|
||||
(simplified) definitions:
|
||||
|
||||
```haskell
|
||||
class Bifunctor p where
|
||||
bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
|
||||
|
||||
class Profunctor p where
|
||||
dimap :: (b -> a) -> (c -> d) -> p a c -> p b d
|
||||
```
|
||||
|
||||
They're identical, except that `bimap` takes a first parameter of type `a ->
|
||||
b`, whereas `dimap` takes a first parameter of type `b -> a`. Based on this
|
||||
observation, and what we've learned previously, we can now understand the
|
||||
documentation for these two typeclasses:
|
||||
|
||||
> `Bifunctor`: Intuitively it is a bifunctor where both the first and second
|
||||
> arguments are covariant.
|
||||
>
|
||||
> `Profunctor`: Intuitively it is a bifunctor where the first argument is
|
||||
> contravariant and the second argument is covariant.
|
||||
|
||||
These are both bifunctors since they take two type parameters. They both treat
|
||||
their second parameter in the same way: covariantly. However, the first
|
||||
parameter is treated differently by the two: `Bifunctor` is covariant, and
|
||||
`Profunctor` is contravariant.
|
||||
|
||||
__Exercise__ Try to think of a few common datatypes in Haskell that would be
|
||||
either a `Bifunctor` or `Profunctor`, and write the instance.
|
||||
|
||||
__Hint__ Some examples are `Either`, `(,)`, and `->` (a normal function from
|
||||
`a` to `b`). Figure out which is a `Bifunctor` and which is a `Profunctor`.
|
||||
|
||||
__Solution__
|
||||
|
||||
```haskell
|
||||
class Bifunctor p where
|
||||
bimap :: (a -> b) -> (c -> d) -> p a c -> p b d
|
||||
|
||||
class Profunctor p where
|
||||
dimap :: (b -> a) -> (c -> d) -> p a c -> p b d
|
||||
|
||||
|
||||
instance Bifunctor Either where
|
||||
bimap f _ (Left x) = Left (f x)
|
||||
bimap _ f (Right x) = Right (f x)
|
||||
instance Bifunctor (,) where
|
||||
bimap f g (x, y) = (f x, g y)
|
||||
|
||||
instance Profunctor (->) where -- functions
|
||||
dimap f g h = g . h . f
|
||||
```
|
||||
|
||||
Make sure you understand *why* these instances work the way they do before
|
||||
moving on.
|
||||
|
||||
## Bivariant and invariant
|
||||
|
||||
There are two more special cases for variance: bivariant means "both covariant
|
||||
and contravariant," whereas invariant means "neither covariant nor
|
||||
contravariant." The only types which can be bivariant are *phantoms*, where the
|
||||
type doesn't actually exist. As an example:
|
||||
|
||||
```haskell
|
||||
import Data.Functor.Contravariant (Contravariant (..))
|
||||
|
||||
data Phantom a = Phantom
|
||||
instance Functor Phantom where
|
||||
fmap _ Phantom = Phantom
|
||||
instance Contravariant Phantom where
|
||||
contramap _ Phantom = Phantom
|
||||
```
|
||||
|
||||
Invariance will usually (always?) occur when a type parameter is used multiple
|
||||
times in the data structure, e.g.:
|
||||
|
||||
```haskell
|
||||
data ToFrom a = ToFrom (a -> Int) (Int -> a)
|
||||
```
|
||||
|
||||
__Exercise__ Convince yourself that you can not make an instance of either `Functor` nor `Contravariant` for this datatype.
|
||||
|
||||
__Exercise__ Explain why there's also no way to make an instance of `Bifunctor` or `Profunctor` for this datatype.
|
||||
|
||||
As you can see, the `a` parameter is used as both the input to a function and
|
||||
output from a function in the above data type. This leads directly to our next
|
||||
set of terms.
|
||||
|
||||
## Positive and negative position
|
||||
|
||||
Let's look at some basic covariant and contravariant data types:
|
||||
|
||||
```haskell
|
||||
data WithInt a = UsesInt (Int -> a)
|
||||
data MakeInt a = MakeInt (a -> Int)
|
||||
```
|
||||
|
||||
By now, you should hopefully be able to identify that `WithInt` is covariant on
|
||||
its type parameter `a`, whereas `MakeInt` is contravariant. Please make sure
|
||||
you're confident of that fact, and that you know what the relevant `Functor`
|
||||
and `Contravariant` instance will be.
|
||||
|
||||
Can we give a simple explanation of why each of these is covariant and
|
||||
contravariant? Fortunately, yes: it has to do with the position the type
|
||||
variable appears in the function. In fact, we can even get GHC to tell us this
|
||||
by using `Functor` deriving:
|
||||
|
||||
```haskell
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
|
||||
data MakeInt a = MakeInt (a -> Int)
|
||||
deriving Functor
|
||||
```
|
||||
|
||||
This results in the (actually quite readable) error message:
|
||||
|
||||
```
|
||||
Can't make a derived instance of ‘Functor MakeInt’:
|
||||
Constructor ‘MakeInt’ must not use the type variable in a function argument
|
||||
In the data declaration for ‘MakeInt’
|
||||
```
|
||||
|
||||
Another way to say this is "`a` appears as an input to the function." An even
|
||||
better way to say this is that "`a` appears in negative position." And now we
|
||||
get to define two new terms:
|
||||
|
||||
* Positive position: the type variable is the result/output/range/codomain of the function
|
||||
* Negative position: the type variable is the argument/input/domain of the function
|
||||
|
||||
When a type variable appears in positive position, the data type is covariant
|
||||
with that variable. When the variable appears in negative position, the data
|
||||
type is contravariant with that variable. To convince yourself that this is
|
||||
true, go review the various data types we've used above, and see if this logic
|
||||
applies.
|
||||
|
||||
But why use the terms positive and negative? This is where things get quite
|
||||
powerful, and drastically simplify your life. Consider the following newtype
|
||||
wrapper intended for callbacks:
|
||||
|
||||
```haskell
|
||||
newtype Callback a = Callback ((a -> IO ()) -> IO ())
|
||||
```
|
||||
|
||||
Is it covariant or contravariant on `a`? Your first instinct may be to say
|
||||
"well, `a` is a function parameter, and therefore it's contravariant. However,
|
||||
let's break things down a bit further.
|
||||
|
||||
Suppose we're just trying to deal with `a -> IO ()`. As we've established many
|
||||
times above: this function is contravariant on `a`, and equivalently `a` is in
|
||||
negative position. This means that this function expects on input of type `a`.
|
||||
|
||||
But now, we wrap up this entire function as the input to a new function, via:
|
||||
`(a -> IO ()) -> IO ()`. As a whole, does this function *consume* an `Int`, or
|
||||
does it *produce* an `Int`? To get an intuition, let's look at an
|
||||
implementation of `Callback Int` for random numbers:
|
||||
|
||||
```haskell
|
||||
supplyRandom :: Callback Int
|
||||
supplyRandom = Callback $ \f -> do
|
||||
int <- randomRIO (1, 10)
|
||||
f int
|
||||
```
|
||||
|
||||
It's clear from this implementation that `supplyRandom` is, in fact,
|
||||
*producing* an `Int`. This is similar to `Maybe`, meaning we have a solid
|
||||
argument for this also being covariant. So let's go back to our
|
||||
positive/negative terminology and see if it explains why.
|
||||
|
||||
In `a -> IO ()`, `a` is in negative position. In `(a -> IO ()) -> IO ()`, `a ->
|
||||
IO ()` is in negative position. Now we just follow multiplication rules: when
|
||||
you multiply two negatives, you get a positive. As a result, in `(a -> IO ())
|
||||
-> IO ()`, `a` is in positive position, meaning that `Callback` is covariant on
|
||||
`a`, and we can define a `Functor` instance. And in fact, GHC agrees with us:
|
||||
|
||||
```haskell
|
||||
{-# LANGUAGE DeriveFunctor #-}
|
||||
import System.Random
|
||||
|
||||
newtype Callback a = Callback
|
||||
{ runCallback :: (a -> IO ()) -> IO ()
|
||||
}
|
||||
deriving Functor
|
||||
|
||||
supplyRandom :: Callback Int
|
||||
supplyRandom = Callback $ \f -> do
|
||||
int <- randomRIO (1, 10)
|
||||
f int
|
||||
|
||||
main :: IO ()
|
||||
main = runCallback supplyRandom print
|
||||
```
|
||||
|
||||
Let's unwrap the magic, though, and define our `Functor` instance explicitly:
|
||||
|
||||
```haskell
|
||||
newtype Callback a = Callback
|
||||
{ runCallback :: (a -> IO ()) -> IO ()
|
||||
}
|
||||
|
||||
instance Functor Callback where
|
||||
fmap f (Callback g) = Callback $ \h -> g (h . f)
|
||||
```
|
||||
|
||||
__Exercise 1__: Analyze the above `Functor` instance and understand what is occurring.
|
||||
|
||||
__Exercise 2__: Convince yourself that the above implementation is the only one
|
||||
that makes sense, and similarly that there is no valid `Contravariant`
|
||||
instance.
|
||||
|
||||
__Exercise 3__: For each of the following newtype wrappers, determine if they
|
||||
are covaraint or contravaraint in their arguments:
|
||||
|
||||
```haskell
|
||||
newtype E1 a = E1 (a -> ())
|
||||
newtype E2 a = E2 (a -> () -> ())
|
||||
newtype E3 a = E3 ((a -> ()) -> ())
|
||||
newtype E4 a = E4 ((a -> () -> ()) -> ())
|
||||
newtype E5 a = E5 ((() -> () -> a) -> ())
|
||||
|
||||
-- trickier:
|
||||
newtype E6 a = E6 ((() -> a -> a) -> ())
|
||||
newtype E7 a = E7 ((() -> () -> a) -> a)
|
||||
newtype E8 a = E8 ((() -> a -> ()) -> a)
|
||||
newtype E9 a = E8 ((() -> () -> ()) -> ())
|
||||
```
|
|
@ -49,6 +49,7 @@ This section demonstrates some common Haskell coding patterns, how they work,
|
|||
when they're useful, and possible pitfalls.
|
||||
|
||||
* [Monad Transformers](../content/monad-transformers.md)
|
||||
* [Covariance, contravariance, and positive and negative position](../content/covariance-contravariance.md)
|
||||
* Continuation Passing Style
|
||||
* Builders and difference lists
|
||||
|
||||
|
|
Loading…
Reference in a new issue