Profunctors for encoding and decoding

Seikkailut
avoltus-kari-pahula.jpg
Kari Pahula
Software Developer
haskell-tausta
In this blog post I’m examining functors in Haskell programming language. This content is fairly technical and some familiarity with Haskell is assumed. Functors are a concept from category theory, though I’m keeping this at a fairly practical level. For some values of practical. When in doubt I’ve erred on the side of verbosity and I would suggest keeping ghci at hand while reading this.

Covariance and contravariance

As with any well behaved program, let’s start with some imports for the following section.

import Data.Functor
import Data.Functor.Contravariant (Contravariant(..))

Consider a basic generic function such as:

f :: a -> b

There is a fundamental difference between the roles of a and b in f: it consumes an a and produces a b. To explore this idea further, let’s fill in the blanks and make a couple of types out of that.

newtype FromInt a = FromInt (Int -> a)
newtype Predicate a = Predicate (a -> Bool)

We can provide some structure for these types to allow operations on them. Both of them can be used to make instances of functors, but since Predicate uses its type parameter for consumption and FromInt for production, they need different kinds of functors. Functors come in two varieties: covariant and contravariant. In broad terms, covariant functors have their arrows going in the same direction and contravariant functors in different directions. In haskell, covariant functors are usually simply called functors. Any data type implementing covariant or contravariant functors needs to provide an implementation for the corresponding type class.

class  Functor f  where
   fmap :: (a -> b) -> f a -> f b

class Contravariant f where
   contramap :: (a -> b) -> f b -> f a

For our example types they can be defined as follows.

instance Functor FromInt where
   fmap f (FromInt g) = FromInt (f . g)

instance Contravariant Predicate where
   contramap f (Predicate g) = Predicate (g . f)

Both instance definitions use function composition to merge the provided function with the functor value. To check that these definitions lead to valid functors we’ll have to make sure that the following properties hold: fmap id = id and contramap id = id. It’s easy to check that f . id = id . f = f for any function f. I’ll examine a bit closer how the fmap instance operates.

-- The definition of function composition, for reference
(.) :: (b -> c) -> (a -> b) -> a -> c

-- Some function, we don’t care of its implementation here
f :: Double -> String
a :: FromInt Double

-- A bit of pseudo-Haskell, you can’t chain definitions like this
fmap f a = fmap f (FromInt g)        
                = FromInt (f . g)

In that last equation, the type of g is Int -> Double. Composing functions means plugging their input and output types like train tracks, therefore we can see that the type of f . g is Int -> String and as such it’s a valid function for FromInt.

To make use of our newly defined functors, let’s create a couple of helper functions to unwrap the contained function.

supplyInt :: FromInt a -> Int -> a
supplyInt (FromInt f) = f

consumeValue :: Predicate a -> a -> Bool
consumeValue (Predicate f) = f

Finally, let’s make a couple of functor values. Strictly speaking, the second one is a function that returns them.

showInt :: FromInt String
showInt = FromInt show

isDivisibleWith :: Int -> Predicate Int
isDivisibleWith n = Predicate ((== 0) . (`rem` n))

With all this in place, we can do some operations with our functors.

*Main> supplyInt (fmap ((<3) . length) showInt) 555
False

Note how the function used with fmap operates on the output side. It can’t do anything to the Int value given to the supplyInt function.

*Main> consumeValue (contramap read (isDivisibleWith 3)) "42"
True

Likewise, the function given to the contramap call operates on its input.

Much of the material in this example is not specific to functor usage or may not even be possible with all functors, and was mostly described for setting up this example. The unwrapping trick done with our helper functions is not available with all functors. A common example where it is Maybe but many functors may be opaque with no such direct access. Also, not all functors give the possibility of constructing them directly, as with FromInt show. Instead, their implementations may provide a set of values that can be used or some functions that return functor values governed by some criteria.

The usual intuition of Functor as class for types that can be mapped over breaks down if you try too directly to think of Contravariant in the same terms. Consider again the type of contramap:

contramap:: (a -> b) -> f b -> f a

If you try to think of applying that to something like a Maybe, you’ll end up in trouble with trying to think of how to modify the result with something that takes as the input something of the type that’s supposed to be the result. It’s not that contramap does something really exotic and unfathomable, it’s just that Maybe won’t give the right idea about what is going on.

Finally, fmap and contramap have infix synonyms which we may use in the rest of this:

(<$>) :: Functor f => (a -> b) -> f a -> f b
(>$<) :: Contravariant f => (a -> b) -> f b -> f a

 

Composing things together: Applicative functors and monoids

Before we move on, there are a couple of further concepts we’ll need to review. There is a way to add more expressiveness to covariant functors, namely applicative functors and the Applicative type class.

class Functor f => Applicative f where
   pure :: a -> f a
   (<*>) :: f (a -> b) -> f a -> f b

They add more composability to a functor. Most common covariant functors are also applicative functors. Typically, if the function used with fmap takes multiple arguments then the rest of the parameters are chained together by <*>. I’ll give a brief example, this time with the Maybe instance.

Prelude> :t (,) <$> Just ()
(,) <$> Just () :: Maybe (b -> ((), b))
Prelude> :t (,) <$> Just () <*> Just "asdf"
(,) <$> Just () <*> Just "asdf" :: Maybe ((), [Char])

Note how the first line gave a functor whose type was a function. As an exercise, try to think of an Applicative instance for FromInt.

There is an analogue for contravariant functors, Divisible, but I won’t mention it further in this material. Instead, Monoid can find use with them:

class Monoid a where
   mempty  :: a
   mappend :: a -> a -> a

And the infix synonym for mappend:

(<>) :: Monoid m => m -> m -> m

In our use, composing contravariant functors is as simple as just concatenating them as monoids.

That was quite a bit of preliminaries to go through, but finally we have everything in place for the main content.

 

Functors in action: Examples with Hasql library

In this section I’ll examine how hasql Postgresql client library uses functors for encoding and decoding. These two operations can be thought of as input and output, respectively. More specifically, encoding is about inputting data from values in Haskell code to some external entity and decoding likewise constructs, or outputs, Haskell values from it.

Decoding

I’ll describe decoding first, which uses covariant functors. Consider an SQL query:

SELECT id, name FROM people WHERE …

Hasql relies on how the return values of a query are ordered in a certain order (like id and name in the above) and allows writing a decoder that takes those parameters in order to build a Haskell value. Hasql.Decoders comes with a set of primitives that represent possible data types that a query may return. To give a few examples:

int4 :: Value Int32
text :: Value Text
uuid :: Value UUID

A value may be used as a part of a result row as is, or with the possibility of having a null. Or even in a composite value or an array value, both of which are supported by Postgresql. The functions for doing the first two operations are as follows:

value :: Value a -> Row a
nullableValue :: Value a -> Row (Maybe a)

A decoder for a simple query like what we had above would be like this:

decoder :: Row (Int32, Text)
decoder = (,) <$> value int4 <*> value text

Note that the order of the values given is significant and is expected to match with what is in the SQL query. We need to still take one step upwards from a Row to get to query results, as we need to express how many rows we expect from the query. To give a few examples of what’s available:

singleRow :: Row a -> Result a
maybeRow :: Row a -> Result (Maybe a)
rowsVector :: Row a -> Result (Vector a)

As discussed earlier, covariant functors deal with output. In this context, we have used covariant functors to describe what to do with the output of an SQL query.

Encoding

Likewise, Hasql uses contravariant functors for encoding query parameters. Again, consider a query with numbered placeholders:

SELECT id, name FROM people WHERE age < $1 and gender = $2 and active = $3

I’m not introducing a schema definition for people in this but take it so that age is an integer, gender is text and active is a boolean. The corresponding data structure in our Haskell code is as follows:

data Person = { age :: Int32, gender :: Text, active :: Bool }

I’ll step through how to set up an encoder to supply those positional parameters. Again, Hasql.Encoders defines a set of primitives. Despite the similar name, these are different definitions in a different source file than the ones decoders uses.

int4 :: Value Int32
text :: Value Text
bool :: Value Bool

As with decoders, these primitives are used to define parameters for a query. Here are the two most common functions:

value :: Value a -> Params a
nullableValue :: Value a -> Params (Maybe a)

This gives the definition for a parameter with a single primitive value or the same wrapped in a Maybe. Contravariant gives us the operation needed to compose our primitives to define encoders for more complex data structures.

encodeAge :: Params Person
encodeAge = age >$< value int4

I’ll slow down to examine this a bit closer. Haskell’s data declaration also defines the matching set of accessor functions for the named fields. In this case, we’re using age :: Person -> Int32.

Note the type of int4 from the above and to reiterate the last actor in this: (>$<) :: (a -> b) -> f b -> f a. Just drop in the types into the contramap call and you should satisfy yourself of that encodeAge has the correct type to take a Person as its input.

The other two encoders can be defined in a similar manner.

encodeGender :: Params Person
encodeGender = gender >$< value text

encodeActive :: Params Person
encodeActive = active >$< value bool

Here we have parameter definitions for a single parameter. They can be thought of as different projections from Person where each value x gives context to how the query encoder should treat the different projected values. Since the types align so nicely composing them to get our desired encoder is as simple as using the Monoid instance.

encoder :: Params Person
encoder = encodeAge <> encodeGender <> encodeActive

Building and running a query

I’m not going into too much detail about how Hasql goes about using and running the queries. An encoder and decoder are used together to define a query, Query a b. Putting together what we defined so far would give the following function.

import qualified Hasql.Encoders as E
import qualified Hasql.Decoders as D
import Hasql.Query

myQuery :: Query Person (Vector (Int32, Text))
myQuery = statement sql encoder decoder True
  where
    sql = "SELECT id, name FROM people \
          \WHERE age < $1 and gender = $2 and active = $3"
    encoder = age >$< E.value E.int4 <>
              gender >$< E.value E.text <>
              active >$< E.value E.bool
    decoder = D.rowsVector $
              (,) <$> D.value D.int4 <*> D.value D.text

The final pair of functions to get all this to IO are

query :: a -> Query a b -> Session b
run :: Session a -> Connection -> IO (Either Error a)

 

Functors for composability

To finish this, I’ll give a few examples of what can be done with decoders defined using functors. I’ll drop contravariant functors from consideration in this section and will use simply “functor” for covariant functors. Let's say we have a web application with user authentication. Our user is represented as thus.

data User =
   { uid :: Int32
   , name :: Text
   , sessionToken :: UUID
   }

data Prefs =
   { rows :: Int32
   , unread :: Maybe (Int32, UTCTime)
   , user :: Maybe User
   }

So far so good.  We have two data types, one for users and another one for settings, which may or may not contain an user. The site may offer a default `rows` setting in case there's no user to use for getting the setting.  This isn't necessarily a good example of how to design data structures in Haskell, but for illustrative purposes I'm going with these two types.

Decoding user data

Expressing a basic decoder for a user login request could be written as such.

userRow :: Row User
userRow = User <$> value int4 <*> value text <*> value uuid

Parametrizing a decoder

Let's say that we are doing a web application which renders its HTML on the server side (“traditional”, if you like) and the user data could be passed along as a session cookie or as login credentials in request parameters. With a session cookie we don't want to query the database what the session token is since we already know it. But we'd like to share the decoder as far as possible and the User data is the same regardless of the authentication method.

userRow' :: Row UUID -> Row User
userRow' x = User <$> value int4 <*> value text <*> x

userLoginRow :: Row User
userLoginRow = userRow' (value uuid)

userSessionRow :: UUID -> Row User
userSessionRow = userRow . pure

This gives us a decoder identical to userRow and a function that returns decoders.

Composing functors

The next step would be to make a decoder for our Prefs data type.  Let's say that we have some pages that always use only the User data and others which make use of Prefs and we want to have decoders for queries that return only User data or both.

prefsRow :: Row UUID -> Row Prefs
prefsRow x =
   Prefs
   <$> value int4
   <*> (liftA tupleOrNothing
        ((,) <$> nullableValue int4 <*> nullableValue timestamptz))
   <*> (liftA Just (userRow' x))

tupleOrNothing :: (Maybe a, Maybe b) -> Maybe (a, b)
tupleOrNothing (a, b) = do
   a' <- a
   b' <- b
   return (a, b)

Again, prefsRow is a function that allows passing the choice of how to get the UUID to userRow'. tupleOrNothing is a helper function that takes a (Maybe a, Maybe b) and returns a Nothing if either of the input values was a Nothing.

liftA :: Applicative f => (a -> b) -> f a -> f b

This is basically just the same function as fmap but for Applicative. prefsRow sees two uses for it, first to combine two Maybe values and then to lift another one to Maybe with a Just.

Wrapping it up

Using applicative functors for writing decoders allows decoupling the decoding logic from its use. They allow composing decoders, adding logic to how they are constructed and to parametrize their definition.

 

Further reading

My motivation in writing this blog post was to give a treatment of contravariant functors where they were introduced on equal footing to the more prominently used covariant functors. I hope that I succeeded at giving a richer perspective to what a covariant functor is about, even if you’re still to see an use for contravariant functors.

I’ve used a number of sources while writing this.

PS. You may be wondering by now why this post was titled “Profunctors” when I never mentioned them. There is, in fact, one included in this all: Query a b. A profunctor is just a bifunctor that is contravariant in the first argument and covariant in the second.