Learn Contravariant in 5 minutes
Functor
Early into learning Haskell, we’re introduced to the Functor
typeclass and
how to fmap
our way around with it. fmap
is a structure-preserving operation
that lets us transform the value wrapped by the structure:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Maybe
has a Functor
instance:
maybeFunctor :: Maybe Int -> Maybe String
= fmap show mInt maybeFunctor mInt
[]
also has a Functor
instance:
listFunctor :: [Int] -> [String]
= fmap show xs listFunctor xs
Even the function arrow has a Functor
instance, when the type on the left-hand
side of the arrow is fixed:
functionFunctor :: (a -> Int) -> (a -> String)
= fmap show f -- a.k.a. (show . f) functionFunctor f
A loosey-goosey intuition is that fmap
lets us map over the output of a
computation.
Contravariant
There is another “mapping” typeclass out there - Contravariant
- that we
likely don’t bump into as often as Functor
:
class Contravariant f where
contramap :: (b -> a) -> f a -> f b
On first blush, if we look at Contravariant
with the more familiar Functor
as our frame of reference, contramap
’s type signature can be very puzzling.
How can we apply a function with type (b -> a)
to the guts of an f a
structure?
A challenge we might face here is that considering we’re likely so used to
common structures that have Functor
instances, we may first try substituting
in those same structures for f
, e.g. Maybe
, []
, and so on. GHC will
quickly tell us that there are no Contravariant
instances for these
structures, so at this point, we’ll need to look at Contravariant
from a new
angle.
Let’s look at a concrete example:
import Control.Exception.Safe (SomeException(SomeException), catch)
import Control.Monad (void)
import Data.Aeson (encode)
import Data.Text (Text)
import Network.HTTP.Client
RequestBody(RequestBodyLBS), Manager, httpNoBody, parseUrlThrow, requestBody
(
)
newtype Processor a = Processor
runProcessor :: a -> IO ()
{
}
type LogMessage = Text
batchLogProcessor :: Manager -> Processor [LogMessage]
=
batchLogProcessor manager Processor
= \logs -> do
{ runProcessor let requestBody = RequestBodyLBS $ encode logs
<- parseUrlThrow "POST http://my-log-aggregation-service.org/post"
req flip httpNoBody manager req { requestBody })
void (`catch` \(SomeException ex) -> do
-- ... do something with the exception here ...
}
The Processor
type wraps a function that takes in as input some value, and
then does something with that value in IO
. We built a batchLogProcessor
that lets us send a batch of logs up to some log aggregation service.
We can also define a convenient (albeit less performant) Processor
that just
sends up one log message at a time, rather than sending a batch. We’ll implement
this one in terms of batchLogProcessor
:
singletonLogProcessor :: Manager -> Processor LogMessage
=
singletonLogProcessor manager Processor
= \logMsg -> do
{ runProcessor
runProcessor (batchLogProcessor manager) [logMsg] }
This type of operation - mapping over the input - is really the essence of
Contravariant
. We can define a Contravariant
instance for Processor
:
instance Contravariant Processor where
contramap :: (b -> a) -> Processor a -> Processor b
=
contramap f inputProcessor Processor
= \x -> do
{ runProcessor $ f x
runProcessor inputProcessor }
With a Contravariant
instance, we can now define singletonLogProcessor
using
contramap
:
singletonLogProcessor :: Manager -> Processor LogMessage
=
singletonLogProcessor manager -> [logMsg]) $ batchLogProcessor manager contramap (\logMsg
We’re also free to contramap
to our heart’s content on all sorts of
Processor
s, not just ones specific to log aggregation.