Providence Salumu
Today and tomorrow we take a brief excursion to the boundary of abstract nonsense with a pair of guest posts from Tom Ellis. Tom, over to you!
In today and tommorow’s posts I’m going to introduce the Contravariant
and Profunctor
type classes, which are found in the contravariant
and profunctors
packages, respectively. They are closely related to the familiar Functor
type class, so let’s refresh our understanding by first reviewing its definition:
class Functor f where
fmap :: (a -> b) -> f a -> f b
We will use the intuition that a Functor
is a sort of “producer of output” that can have its type adapted. If f
is a functor, then f a
represents a “producer” of type a
. Using a normal Haskell function of type a -> b
we can adapt the output to the type b
by using fmap
. Here are three examples of the “producer of output” intuition for a Functor
:
Maybe a
represents an output of type a
which may be either present (Just a
) or absent (Nothing
). Using fmap f :: Maybe a -> Maybe b
we can adapt that potentially absent a
into a potentially absent b
.
[a]
represents a sequence of output values of type a
, and with the Functor
instance for []
we can adapt them to a sequence of output values of type b
.
A value of type r -> a
is a function taking an input of type r
and giving an output of type a
. The Functor
instance for (->) r
- which is the type of functions taking r
as input - allows us to convert this output to type b
, by composition:
instance Functor ((->) r) where
fmap f g = f . g
Incidentally, this is also the underlying Functor
instance of the reader monad.
While fmap
allowed us to change the result type of the function, notice how we are unable to change the input type. The Functor
type class does not allow to change the input, and that’s where Contravariant
comes in. The intuition behind a Contravariant
is that it reflects a sort of “consumer of input” that can have its type adapted. For a contravariant functor f
, f a
represents some sort of input of type a
. As with fmap
, we can use contramap
with a function of type a -> b
to change, we can adapt the input to the type b
.
class Contravariant f where
contramap :: (b -> a) -> f a -> f b
The function arrow is the prototypical example of something contravariant, although because of the ordering of the type parameters, we have to give it a newtype
, which is commonly called Op
(for Op
posite):
newtype Op z a = Op (a -> z)
instance Contravariant (Op z) where
contramap h (Op g) = Op (g . h)
For example, we can use contramap
to map over the input of a function that calculates the length of a list, to calculate the length of a Set
:
lengther :: Op Int [a]
lengther = Op Prelude.length
setLengther :: Op Int (Set a)
setLengther = contramap Set.toList lengther
In Haskell, we like to be able to reason about our code, so most type classes come with corresponding laws. The laws that the instance is required to satisfy are dual to the Functor
laws. First, let’s recall what the Functor
laws are:
fmap id = id
fmap (f . g) = fmap f . fmap g
The idea of “dual” laws here means that the order of the f
and g
switches over, whereas in the functor law they keep their relative ordering. Therefore, the laws for contravariant functors are:
contramap id = id
contramap (f . g) = contramap g . contramap f
A more interesting example of a contravariant functor is an actor that receives messages of type a
, performs some action and then listens for further messages of type a
, repeating this process indefinitely. (You’ll find a similar definition to this in the simple-actors
package).
data Behavior a = Behavior (a -> IO (Behavior a))
instance Contravariant Behavior where
contramap f (Behavior r) = Behavior (\a -> fmap (contramap f) (r (f a)))
runBehavior :: Behavior a -> [a] -> IO ()
runBehavior _ [] = return ()
runBehavior (Behavior f) (a:as) = do
newBehavior <- f a
runBehavior newBehavior as
We can make a Behavior
that just prints the strings it receives:
printer :: Behavior String
printer = Behavior (\s -> putStrLn s >> return printer)
messages :: [String]
messages = [ "Hello", "world", "Haskell", "is", "great" ]
testPrinter = runBehavior printer messages
We run our printer
with testPrinter :: IO ()
, which gives the following output:
> testPrinter
Hello
world
Haskell
is
great
Using contramap
we can adapt it to a Behaviour
which shouts:
shoutyPrinter :: Behavior String
shoutyPrinter = contramap (\s -> (map toUpper s ++ "!")) printer
testShoutyPrinter = runBehavior shoutyPrinter messages
Who’s output is now a bit louder:
> testShoutyPrinter
HELLO!
WORLD!
HASKELL!
IS!
GREAT!
A more complicated example would be a “mailbox” that listens for messages and prints out the messages it has received so far. These mailboxes will have a limit on the amount of messages they can hold, and if attempt to push messages into a full mailbox, then the message is dropped. We can model this as another Behaviour
:
makeMailbox :: [(String, String)] -> Behavior (String, String)
makeMailbox messages = Behavior $ \message ->
if length messages < 3
then let newMessages = messages ++ [message]
in do putStrLn "I contain messages:"
mapM_ printMessage newMessages
return (makeMailbox newMessages)
else do putStrLn "I am full."
putStrLn "I contain messages:"
mapM_ printMessage messages
return (makeMailbox messages)
where
printMessage (from, message) =
putStrLn ("From " ++ from ++ ": " ++ message)
mailbox :: Behavior (String, String)
mailbox = makeMailbox []
Now we test the mailbox. There are some messages we don’t want to receive, but luckily the mailbox gets full before they arrive!
testMailbox = runBehavior mailbox messages
where messages = [ ("ocharles", "hackage is great")
, ("edwardk", "I love Simon Peyton Jones")
, ("spj", "We all love lazy evaluation")
, ("Your spouse", "Make me a cup of tea")
, ("Your employer", "You must program in Scala") ]
If we run this, we’ll see that the first 3 messages are delivered, but after that the messages are dropped:
> testMailbox
I contain messages:
From ocharles: hackage is great
I contain messages:
From ocharles: hackage is great
From edwardk: I love Simon Peyton Jones
I contain messages:
From ocharles: hackage is great
From edwardk: I love Simon Peyton Jones
From spj: We all love lazy evaluation
I am full.
I contain messages:
From ocharles: hackage is great
From edwardk: I love Simon Peyton Jones
From spj: We all love lazy evaluation
I am full.
I contain messages:
From ocharles: hackage is great
From edwardk: I love Simon Peyton Jones
From spj: We all love lazy evaluation
A mailbox expects to receive a message consisting of a tuple of (String, String)
containing the “sender name” and “message body”. As long as we can provide a function a -> (String, String)
we can use contramap
to make a Behavior a
from our mailbox. Here’s an example of using the mailbox as a logger. A log entry is either success carrying an integer, or a failure message. We can turn this into a message for a mailbox with a simple logMsg
function:
logMsg :: Either String Int -> (String, String)
logMsg (Right x) = ("Logger daemon", "Everything is OK: " ++ show x)
logMsg (Left s) = ("Logger daemon", "FAILURE: " ++ s)
Now we can contramap
our previous mailbox into a mailbox that accepts log entries as input:
loggerMailbox :: Behavior (Either String Int)
loggerMailbox = contramap logMsg mailbox
testLogger = runBehavior loggerMailbox messages
where messages = [ Right 24
, Left "Oops, there was an error"
, Right 1
, Right 2 ]
A final run of this testLogger
shows:
> testLogger
I contain messages:
From Logger daemon: Everything is OK: 24
I contain messages:
From Logger daemon: Everything is OK: 24
From Logger daemon: FAILURE: Oops, there was an error
I contain messages:
From Logger daemon: Everything is OK: 24
From Logger daemon: FAILURE: Oops, there was an error
From Logger daemon: Everything is OK: 1
I am full.
I contain messages:
From Logger daemon: Everything is OK: 24
From Logger daemon: FAILURE: Oops, there was an error
From Logger daemon: Everything is OK: 1
There are not many instances of Contravariant
on Hackage, although there are surely many contravariant type parameters, and perhaps this type class could be used more widely.