Tinkering with lenses to deal with API changes.
Literate Haskell source for this post: https://github.com/carlohamalainen/playground/tree/master/haskell/lens-has.
First, some extensions and imports.
Suppose we are working with a database service that stores files. Perhaps we communicate with it via a REST API. A file stored in the system has a location, which is a
We need to keep track of a few other things like the parent (referring to a collection of files) and a hash of the file. For simplicity I’ll make those two fields
Strings since the details aren’t important to us here.
(Ignore the underscores if you haven’t used lenses before.)
After some time the API changes and we need to keep track of some different fields, so our data type changes to:
For compatibility we’d like to keep both definitions around, perhaps allowing the user
to choose the v1 or v2 API with a configuration option. So how do we deal with our code that
has to use
DataFile2? One option is to use a sum type:
Any function that uses a
DataFile must instead
DataFileSum and do case analysis on whether it is
a v1 or v2.
In my particular situation I had a number of functions that used
Location part of the type. Is there a way to
avoid the sum type?
Use typeclasses to represent setting or getting the location value:
Write the instance definitions for each case:
Now we use the general
setLocation functions instead of
the specific data constructors of
A function that uses a datafile can now be agnostic about which one it is, as long as the typeclass constraint is satisfied so that it has the appropriate getter/setter:
*LensHas> doSomething $ DataFile "/foo/bar.txt" "parent" "12345" "/foo/bar.txt" *LensHas> doSomething $ DataFile2 "/foo/bar.txt" "parent" 42.2 "/foo/bar.txt"
Lenses already deal with the concept of getters and setters, so let’s try to replicate the previous code in that framework.
First, make lenses for the two data types (this uses Template Haskell):
makeLenses ''DataFile makeLenses ''DataFile2
Instead of type classes for setting and getting, make a single type class that represents the fact that a thing has a location.
For the instance definitions we can use the lenses that were automatically made for us by the earlier
main1 rewritten to use the
If you haven’t used lenses before the operators like
^. might look insane, but there is a pattern to them. Check out http://intolerable.me/lens-operators-intro for an excellent guide with examples.
One benefit of the lens approach is that we don’t have to
manually write the setters and getters, as they come for free
from the lenses for the original two data types. Another benefit
is that lenses compose, so if the
Location type was more than just a string,
we wouldn’t have to manually deal with the composition of
getSubPartOfLocation and so on.
doSomething function can be rewritten using the
Let’s generalise the
HasLocation typeclass. Consider natural numbers (the
First case: here’s a typeclass to represent the fact
Foo can always be thought of as a
Second case: a natural is a natural by definition.
Third case: an
Integer might be a
Natural. The previous typeclasses used a
Lens' but here
we need a
We are doing much the same thing, and if we compare the two typeclasses the difference is in the type of “optical” thing being used (a lens or a prism):
It turns out that the type to use is
class AsNatural p f s where natural :: Optic’ p f s Natural
(We get the extra parameters
f which seem to be unavoidable.)
Now we can do all of the previous definitions using the single typeclass:
Now we can work with a
Integer as a
Natural by using the single
*LensHas> main3 34 35 43 Just 51 Nothing
AsNatural type is a simplified version of the
As... typeclasses in the coordinate package, e.g. AsMinutes. Thanks to Tony Morris on #haskell.au for helping with my changing-API question and pointing out the
As... typeclasses. Also see the IRC logs in coordinate/etc where Ed Kmett explains some things about Optic.