A "lens" and a "partial lens" seem rather similar in name and in concept. How do they differ? In what circumstances do I need to use one or the other?
Tagging Scala and Haskell, but I'd welcome explanations related to any functional language that has a lens library.
Scalaz documentation
Below are the scaladocs for Scalaz's
LensFamily
andPLensFamily
, with emphasis added on the diffs.Lens:
Partial lens:
Notation
For those unfamiliar with scalaz, we should point out the symbolic type aliases:
In infix notation, this means the type of a lens that retrieves a field of type
B
from a record of typeA
is expressed asA @> B
, and a partial lens asA @?> B
.Argonaut
Argonaut (a JSON library) provides a lot of examples of partial lenses, because the schemaless nature of JSON means that attempting to retrieve something from an arbitrary JSON value always has the possibility of failure. Here are a few examples of lens-constructing functions from Argonaut:
def jArrayPL: Json @?> JsonArray
— Retrieves a value only if the JSON value is an arraydef jStringPL: Json @?> JsonString
— Retrieves a value only if the JSON value is a stringdef jsonObjectPL(f: JsonField): JsonObject @?> Json
— Retrieves a value only if the JSON object has the fieldf
def jsonArrayPL(n: Int): JsonArray @?> Json
— Retrieves a value only if the JSON array has an element at indexn
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 ans
we can "focus" on a subcomponent ofs
at typea
, viewing it, replacing it, and (if we use the lens family variationLens s t a b
) even changing its type.One way to look at this is that
Lens s a
witnesses an isomorphism, an equivalence, betweens
and the tuple type(r, a)
for some unknown typer
.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 news
with out updateda
.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 ana
and ab
and we writea + b
when we have a type consisting of items which are eithera
orb
.In Haskell we write
a * b
as(a, b)
, the tuple type. We writea + b
asEither 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.
This is a declaration that
s
is either a typea
or some other thingr
. We've got alens
-like thing that embodies the notion of option (and of failure) deep at it's core.This is exactly a prism (or partial lens)
So how does this work concerning some simple examples?
Well, consider the prism which "unconses" a list:
it's equivalent to this
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 ana
into ab
and ab
into ana
such that they invert one another. Let's write that in order to describe our prism via the mythological functionThis 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.
A lens is a "functional reference" that allows you to extract and/or update a generalized "field" in a larger value. For an ordinary, non-partial lens that field is always required to be there, for any value of the containing type. This presents a problem if you want to look at something like a "field" which might not always be there. For example, in the case of "the nth element of a list" (as listed in the Scalaz documentation @ChrisMartin pasted), the list might be too short.
Thus, a "partial lens" generalizes a lens to the case where a field may or may not always be present in a larger value.
There are at least three things in the Haskell
lens
library that you could think of as "partial lenses", none of which corresponds exactly to the Scala version:Lens
whose "field" is aMaybe
type.Prism
, as described by @J.Abrahamson.Traversal
.They all have their uses, but the first two are too restricted to include all cases, while
Traversal
s are "too general". Of the three, onlyTraversal
s support the "nth element of list" example.For the "
Lens
giving aMaybe
-wrapped value" version, what breaks is the lens laws: to have a proper lens, you should be able to set it toNothing
to remove the optional field, then set it back to what it was, and then get back the same value. This works fine for aMap
say (andControl.Lens.At.at
gives such a lens forMap
-like containers), but not for a list, where deleting e.g. the0
th element cannot avoid disturbing the later ones.A
Prism
is in a sense a generalization of a constructor (approximately case class in Scala) rather than a field. As such the "field" it gives when present should contain all the information to regenerate the whole structure (which you can do with thereview
function.)A
Traversal
can do "nth element of a list" just fine, in fact there are at least two different functionsix
andelement
that both work for this (but generalize slightly differently to other containers).Thanks to the typeclass magic of
lens
, anyPrism
orLens
automatically works as aTraversal
, while aLens
giving aMaybe
-wrapped optional field can be turned into aTraversal
of a plain optional field by composing withtraverse
.However, a
Traversal
is in some sense too general, because it is not restricted to a single field: ATraversal
can have any number of "target" fields. E.g.is a
Traversal
that will happily go through all the odd-indexed elements of a list, updating and/or extracting information from them all.In theory, you could define a fourth variant (the "affine traversals" @J.Abrahamson mentions) that I think might correspond more closely to Scala's version, but due to a technical reason outside the
lens
library itself they would not fit well with the rest of the library - you would have to explicitly convert such a "partial lens" to use some of theTraversal
operations with it.Also, it would not buy you much over ordinary
Traversal
s, since there's e.g. a simple operator(^?)
to extract just the first element traversed.(As far as I can see, the technical reason is that the
Pointed
typeclass which would be needed to define an "affine traversal" is not a superclass ofApplicative
, which ordinaryTraversal
s use.)