A
Divisible contravariant functor is the contravariant analogue
of
Applicative.
Continuing the intuition that
Contravariant functors consume
input, a
Divisible contravariant functor also has the ability
to be composed "beside" another contravariant functor.
Serializers provide a good example of
Divisible contravariant
functors. To begin let's start with the type of serializers for
specific types:
newtype Serializer a = Serializer { runSerializer :: a -> ByteString }
This is a contravariant functor:
instance Contravariant Serializer where
contramap f s = Serializer (runSerializer s . f)
That is, given a serializer for
a (
s :: Serializer
a), and a way to turn
bs into
as (a mapping
f :: b -> a), we have a serializer for
b:
contramap f s :: Serializer b.
Divisible gives us a way to combine two serializers that focus on
different parts of a structure. If we postulate the existance of two
primitive serializers -
string :: Serializer String and
int :: Serializer Int, we would like to be able to combine
these into a serializer for pairs of
Strings and
Ints. How can we do this? Simply run both serializers and
combine their output!
data StringAndInt = StringAndInt String Int
stringAndInt :: Serializer StringAndInt
stringAndInt = Serializer $ \(StringAndInt s i) ->
let sBytes = runSerializer string s
iBytes = runSerializer int i
in sBytes <> iBytes
divide is a generalization by also taking a
contramap
like function to split any
a into a pair. This conveniently
allows you to target fields of a record, for instance, by extracting
the values under two fields and combining them into a tuple.
To complete the example, here is how to write
stringAndInt
using a
Divisible instance:
instance Divisible Serializer where
conquer = Serializer (const mempty)
divide toBC bSerializer cSerializer = Serializer $ \a ->
case toBC a of
(b, c) ->
let bBytes = runSerializer bSerializer b
cBytes = runSerializer cSerializer c
in bBytes <> cBytes
stringAndInt :: Serializer StringAndInt
stringAndInt =
divide (\(StringAndInt s i) -> (s, i)) string int