To describe partial lenses—which I will henceforth call, according to the Haskell lens
nomenclature, prisms (excepting that they're not! See the comment by Ørjan)—I'd like to begin by taking a different look at lenses themselves.
A lens Lens s a
indicates that given an s
we can "focus" on a subcomponent of s
at type a
, viewing it, replacing it, and (if we use the lens family variation Lens s t a b
) even changing its type.
One way to look at this is that Lens s a
witnesses an isomorphism, an equivalence, between s
and the tuple type (r, a)
for some unknown type r
.
Lens s a ====== exists r . s ~ (r, a)
This gives us what we need since we can pull the a
out, replace it, and then run things back through the equivalence backward to get a new s
with out updated a
.
Now let's take a minute to refresh our high school algebra via algebraic data types. Two key operations in ADTs are multiplication and summation. We write the type a * b
when we have a type consisting of items which have both an a
and a b
and we write a + b
when we have a type consisting of items which are either a
or b
.
In Haskell we write a * b
as (a, b)
, the tuple type. We write a + b
as Either a b
, the either type.
Products represent bundling data together, sums represent bundling options together. Products can represent the idea of having many things only one of which you'd like to choose (at a time) whereas sums represent the idea of failure because you were hoping to take one option (on the left side, say) but instead had to settle for the other one (along the right).
Finally, sums and products are categorical duals. They fit together and having one without the other, as most PLs do, puts you in an awkward place.
So let's take a look at what happens when we dualize (part of) our lens formulation above.
exists r . s ~ (r + a)
This is a declaration that s
is either a type a
or some other thing r
. We've got a lens
-like thing that embodies the notion of option (and of failure) deep at it's core.
This is exactly a prism (or partial lens)
Prism s a ====== exists r . s ~ (r + a)
exists r . s ~ Either r a
So how does this work concerning some simple examples?
Well, consider the prism which "unconses" a list:
uncons :: Prism [a] (a, [a])
it's equivalent to this
head :: exists r . [a] ~ (r + (a, [a]))
and it's relatively obvious what r
entails here: total failure since we have an empty list!
To substantiate the type a ~ b
we need to write a way to transform an a
into a b
and a b
into an a
such that they invert one another. Let's write that in order to describe our prism via the mythological function
prism :: (s ~ exists r . Either r a) -> Prism s a
uncons = prism (iso fwd bck) where
fwd [] = Left () -- failure!
fwd (a:as) = Right (a, as)
bck (Left ()) = []
bck (Right (a, as)) = a:as
This demonstrates how to use this equivalence (at least in principle) to create prisms and also suggests that they ought to feel really natural whenever we're working with sum-like types such as lists.