As we've brought up a Twirp-based API, we've found a need to convert
between several different "views" of the same data, such as position
or span information. Because all protobuf fields are optional, we have
to juggle the `Maybe` values associated with the protobuf fields that
we are converting. While I think this approach has merit, there is a
complexity overhead associated with these conversions: we currently
have around ten ad-hoc functions that perform these conversions, often
containing superfluous `Maybe`s for the sake of convenience.
I've replaced these ad-hoc functions with two classes: `APIBridge` and
`APIConvert`. An instance of `APIBridge` between types `a` and `b`
means that we can convert between `a` and `b` and vice versa
losslessly; in other words, there is an isomorphism between them.
`APIConvert` means that you can convert from an `a` to a `b`, but you
may not be able to convert from all `b`s to an `a` (such as in the
case of missing fields); in other words, there is a partial isomorphism.
These are implemented with concepts from `lens`, namely an `Iso` for
`APIBridge` and a `Prism` for `APIConvert`.
Advantages of this approach:
* Lawful API. We can now clearly delineate the fact that converting a
native data type to an API data type always succeeds, but the
reverse may fail: an API `Span` may have missing position
information, and we want to handle that explicitly, rather than
paper over it with these helper functions. Both the APIBridge and
APIConvert typeclasses provide a set of strong laws re. behavior,
since they provide a lens-y interface.
* Unified API. No longer do we have to juggle a set of functions in
our heads - no need to choose between `spanToSpan`,
`spanToLegacySpan`, or `apiSpanToSpan`. `converting` and `bridging`
do the work for you. Everything is much cleaner.
* Fewer partial functions. The converter from API blob pairs to native
blob pairs no longer calls `error`, which is definitely a good
thing.
* Historical precedent. Prisms and isomorphisms are a fluent way to
express data transformations; the team behind Minecraft uses
isomorphisms and prisms [to transfer data between versions][minecraft].
Disadvantages:
* Complexity overhead. You have to learn about prisms, reviews,
isomorphisms, neither of which is the world's hardest concept but
which take a little while to internalize.
* This might be polymorphism for polymorphism's sake.
Something we could do is postpone this patch until I have a chance to
give a lens tutorial in a Codex.
[minecraft]: https://github.com/Mojang/DataFixerUpper
The `BlobPair` type is defined as an alias for `Join These Blob`. Though
this sacrifices a degree of type safety, it's extremely convenient, as
you can get to a Functor and Bifunctor instance very quickly.
Pattern-matching on `BlobPair` is less elegant though, as it requires
a nested Join then a match on `These`, which is not immediately
indicative of what a given pair might do.
This adds pattern synonyms for the `Inserting`, `Deleting`, and
`Diffing` cases, and removes the less-expressive functions returning such.