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.
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 a Maybe
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, only Traversal
s support the "nth element of list" example.
For the "Lens
giving a Maybe
-wrapped value" version, what breaks is the lens laws: to have a proper lens, you should be able to set it to Nothing
to remove the optional field, then set it back to what it was, and then get back the same value. This works fine for a Map
say (and Control.Lens.At.at
gives such a lens for Map
-like containers), but not for a list, where deleting e.g. the 0
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 the review
function.)
A Traversal
can do "nth element of a list" just fine, in fact there are at least two different functions ix
and element
that both work for this (but generalize slightly differently to other containers).
Thanks to the typeclass magic of lens
, any Prism
or Lens
automatically works as a Traversal
, while a Lens
giving a Maybe
-wrapped optional field can be turned into a Traversal
of a plain optional field by composing with traverse
.
However, a Traversal
is in some sense too general, because it is not restricted to a single field: A Traversal
can have any number of "target" fields. E.g.
elements odd
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 the Traversal
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 of Applicative
, which ordinary Traversal
s use.)
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.
Below are the scaladocs for Scalaz's LensFamily
and PLensFamily
, with emphasis added on the diffs.
Lens:
A Lens Family, offering a purely functional means to access and retrieve a field transitioning from type
B1
to typeB2
in a record simultaneously transitioning from typeA1
to typeA2
.scalaz.Lens
is a convenient alias for whenA1 =:= A2
, andB1 =:= B2
.The term "field" should not be interpreted restrictively to mean a member of a class. For example, a lens family can address membership of a
Set
.
Partial lens:
Partial Lens Families, offering a purely functional means to access and retrieve an optional field transitioning from type
B1
to typeB2
in a record that is simultaneously transitioning from typeA1
to typeA2
.scalaz.PLens
is a convenient alias for whenA1 =:= A2
, andB1 =:= B2
.The term "field" should not be interpreted restrictively to mean a member of a class. For example, a partial lens family can address the nth element of a
List
.
For those unfamiliar with scalaz, we should point out the symbolic type aliases:
type @>[A, B] = Lens[A, B]
type @?>[A, B] = PLens[A, B]
In infix notation, this means the type of a lens that retrieves a field of type B
from a record of type A
is expressed as A @> B
, and a partial lens as A @?> B
.
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 field f
def jsonArrayPL(n: Int): JsonArray @?> Json
— Retrieves a value only if the JSON array has an element at index n
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With