
Goals
This series explores the building blocks of Profunctors without requiring Category theory background. After assembling the parts, you should see how packaging these operators as a pair allows library designers to offer a declarative interface for building custom codecs with a small number of primitives.
This gives us the ability to build larger programs from smaller pieces without keeping the entire system in our heads as we build the glue of our application.
Motivation Through Examples
Profunctors are a practical abstraction for many activities in software development. This first part introduces motivating examples that developers face in typical application work.
Software development for web/native apps and backend services often involves paired operations:
Encoding and Decoding
Binary data cannot travel through text-based protocols unchanged. Base64 encoding converts binary data into text, and decoding reverses the transformation without loss.
On the command-line:
$ printf "some data that needs to be encoded to Base64" | base64
c29tZSBkYXRhIHRoYXQgbmVlZHMgdG8gYmUgZW5jb2RlZCB0byBCYXNlNjQ=
$ printf "c29tZSBkYXRhIHRoYXQgbmVlZHMgdG8gYmUgZW5jb2RlZCB0byBCYXNlNjQ=" | base64 --decode
some data that needs to be encoded to Base64In Haskell (using the base64 package):
>>> :set -XOverloadedStrings
>>> import Data.ByteString.Base64
>>> encodeBase64' "some data that needs to be encoded to Base64"
"c29tZSBkYXRhIHRoYXQgbmVlZHMgdG8gYmUgZW5jb2RlZCB0byBCYXNlNjQ="
>>> decodeBase64 "c29tZSBkYXRhIHRoYXQgbmVlZHMgdG8gYmUgZW5jb2RlZCB0byBCYXNlNjQ="
Right "some data that needs to be encoded to Base64"Serializing and Deserializing
We transfer structured data over the wire from client to server and back. Both sides must agree on serialization and deserialization formats.
In Haskell (using the aeson package):
>>> :set -XOverloadedStrings
>>> :set -XDeriveGeneric
>>> import GHC.Generics
>>> import Data.Text (Text)
>>> import Data.Aeson
>>> data User = User { username :: Text, bio :: Text } deriving (Generic, Show)
>>> instance ToJSON User where toEncoding = genericToEncoding defaultOptions
>>> instance FromJSON User
>>> let user = User "mbbx6spp" "Principal developer who loves Haskell and Nix."
>>> let json = encode user
>>> json
"{\"username\":\"mbbx6spp\",\"bio\":\"Principal developer who loves Haskell and Nix.\"}"
>>> decode json :: Maybe User
Just (User {username = "mbbx6spp", bio = "Principal developer who loves Haskell and Nix."})Encrypting and Decrypting
We encrypt data so that if intercepted by a third party, recovering the original message is computationally difficult without the key.
On the command-line:
# generate random IV
$ openssl rand -hex 16 > iv.txt
# generate random key
$ openssl rand -base64 256 > key.txt
# encrypt "hello world"
$ openssl enc \
-aes-256-cbc -pbkdf2 -salt \
-kfile key.txt \
-iv "$(<iv.txt)" \
<<<"hello world" \
> encrypted.txt
# decrypt using same algorithm, key, and IV
$ openssl enc -d \
-aes-256-cbc -pbkdf2 -salt \
-kfile key.txt \
-iv "$(<iv.txt)" \
<encrypted.txt
hello worldSigning and Validating
We produce content that another party can validate as authored by us and untampered.
Using GnuPG:
# Create content to sign
$ echo "important document content" > content.txt
# Create a detached signature with PGP signing key
$ gpg --detach-sign --armor --output sig.txt content.txt
gpg: using "XXXXXXXX" as default secret key for signing
# Verify detached signature corresponds to the content
$ gpg --verify sig.txt content.txt
gpg: Signature made Sun 09 Aug 2020 04:39:45 PM CDT
gpg: using RSA key XXXXXXXX
gpg: Good signature from "Susan Potter <email@domain.tld>" [ultimate]Parsing and Printing
Structured data often needs a text representation. Parsing converts text into a structured form; printing converts the structure back to text.
>>> import Text.Read (readMaybe)
>>> readMaybe "42" :: Maybe Int
Just 42
>>> show (42 :: Int)
"42"
>>> readMaybe (show 42) :: Maybe Int
Just 42More complex examples include date parsing, URL parsing, and configuration file formats. Each requires a parser (text to structure) and a printer (structure to text) that agree on the format.
The Pattern
These paired operations share a structure:
| Domain | Forward | Backward |
|---|---|---|
| Encoding | encode | decode |
| Serialization | serialize | deserialize |
| Cryptography | encrypt | decrypt |
| Signatures | sign | validate |
| Parsing | parse |
Each pair transforms data in one direction and reverses that transformation in the other. The operations are not independent; they must be consistent with each other. Encoding followed by decoding should return the original data (for lossless encodings). Encrypting then decrypting with the same key recovers the plaintext.
This "round-tripping" pattern appears throughout software development. Profunctors provide a unified abstraction for these paired transformations.
Next
In Part 2, we explore what makes these pairings similar, how they differ, and how a common interface benefits the practitioner.