Susan Potter

Profunctors for the web developer: Motivating examples, Part 1

Tue September 9, 2020

DRAFT

/images/yin-yang-small.webp

Photo by Alex on Unsplash

Notes:

Goals

This series explores one kind of motivation and the building blocks of Profunctors with an aim to introduce the concepts informally while introducing terminology along the way to software development practitioners that may not be familiar with the Category theory building blocks that Profunctors are based on. After assembling the parts of a Profunctor the reader should start to see how packaging these operators as a pair allows library designers to offer a declarative interface for their clients to build custom codecs with a small number of primitives.

This gives us the ability to build larger programs from smaller pieces without needing to keep a large amount of state in our heads as we build he glue of our application.

Motivation through examples

Profunctors are a remarkably practical abstraction for many activities that software developers are concerned with when building products. This first part of the Profunctor series will introduce example motivations many software developers face when doing typical application development.

Today a significant amount of software development in-the-small for web/native apps or backend services concerns the following activity pairs:

Encoding & decoding

We need to ensure that encoding is consistent, e.g. base64 encoding allows us to convert binary data into text without loss.

On the command-line we might do:

$ base64 <<<"some data that needs to be encoded to Base64"
c29tZSBkYXRhIHRoYXQgbmVlZHMgdG8gYmUgZW5jb2RlZCB0byBCYXNlNjQK

$ base64 --decode <<<"c29tZSBkYXRhIHRoYXQgbmVlZHMgdG8gYmUgZW5jb2RlZCB0byBCYXNlNjQK"
some data that needs to be encoded to Base64

In Haskell we might write (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 & deserializing

We need to transfer structured data over the wire from client ot server and back again. Thus we must have an agreement on serialization and deserialization formats on both sides.

In Haskell we might write (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 <3s Haskell, Nix."
>>> let json = encode user
>>> json
"{\"username\":\"mbbx6spp\",\"bio\":\"Principal developer who <3s Haskell, Nix.\"}"
>>> decode json :: Maybe User -- you must annotate the type when it can't be inferred
Just (User {username = "mbbx6spp", bio = "Principal developer who <3s Haskell, Nix."})
Encrypting & decrypting

We sometimes need to encrypt a piece of data so we can send it to another party such that if it is intercepted by a third party it would be [computationally] difficult to determine the original message without more security layer context.

On the command-line we might do the following:

# 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 the encrypted data using same algo+key+IV as encrypt
$ openssl enc -d \
    -aes-256-cbc -pbkdf2 -salt \
    -kfile key.txt \
    -iv "$(<iv.txt)" \
    <encrypted.txt
hello world
Signing & validating

We need to produce content that we can identify as having written such that another party can validate to some degree of confidence that it was us who authored the content and the content has not be tampered with.

Using GnuPG (gpg) command-line tools we can sign and validate a document as follows:

# Create some amazing content I need to sign
$ echo "jake the dog and fin the human" > sig.txt

# Create a detached signature of the content with my PGP signing key
$ gpg --detach-sign --armor --output sig.txt <content.txt
gpg: using "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" as default secret key for signing

# Verify detached signature correspoonds 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 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
gpg: Good signature from "Susan Potter <YY@domain.tld>" [ultimate]
gpg:                 aka "keybase.io/mbbx6spp <mbbx6spp@keybase.io>" [ultimate]
Primary key fingerprint: 9999 AAAA BBBB CCCC DDDD  EEEE FFFF 0000 1111 2222
     Subkey fingerprint: 1111 2222 3333 4444 5555  6666 7777 8888 9999 AAAA
Requesting & responding

web developers are very familiar with the sending requests and making sense of responses with HTTP among other protocols. To request the resource at /humans.txt on https://susanpotter.net curl opened a TLS connection to port 443 (default HTTPS port) and sent the HTTP request GET /humans plus added some HTTP request headers under the covers. It then received the response, and printed the response code, HTTP response headers, and response body to stdout.

$ curl -sSi https://susanpotter.net/humans.txt
HTTP/2 200
content-type: text/plain; charset=utf-8
last-modified: Tue, 22 Dec 2020 22:05:26 GMT
access-control-allow-origin: *
etag: "5fe26da6-c9"
expires: Mon, 11 Jan 2021 02:38:39 GMT
cache-control: max-age=600
x-proxy-cache: MISS
x-github-request-id: 9C86:53DF:58F367:6C643E:5FFBB7D7
accept-ranges: bytes
date: Mon, 11 Jan 2021 02:36:26 GMT
via: 1.1 varnish
age: 467
x-served-by: cache-pwk4960-PWK
x-cache: HIT
x-cache-hits: 1
x-timer: S1610332586.139836,VS0,VE1
vary: Accept-Encoding
x-fastly-request-id: b694ec498e090e7f52fd5f7591c08b613a473f72
content-length: 201

/* whoami */
Owner & Developer: Susan Potter.
Site: https://susanpotter.net
/* Come heckle me */
Twitter: @SusanPotter
Github:  https://github.com/mbbx6spp
Keybase: https://keybase.io/mbbx6spp
/* Endorse me for skills like: Typesafe Jira Comments, Highly Manicured Exit Blogging, or Functional Programming */
LinkedIn: https://www.linkedin.com/in/susanpotter/
Location: IL (USA)

So far we have looked at different types of "round tripping" operation pairs we frequently find ourselves exercising when we build web or native applications and backend services. It seems like maybe these operations could be packages together to provide a more coherent API that transcends the specific use case.

In the next part of this Profunctor series, we will explore the "round-tripping" pattern further through these examples.