What's the difference between a lens and a par

2020-05-26 10:12发布

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.

3条回答
戒情不戒烟
2楼-- · 2020-05-26 10:54

Scalaz documentation

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 type B2 in a record simultaneously transitioning from type A1 to type A2. scalaz.Lens is a convenient alias for when A1 =:= A2, and B1 =:= 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 type B2 in a record that is simultaneously transitioning from type A1 to type A2. scalaz.PLens is a convenient alias for when A1 =:= A2, and B1 =:= 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.

Notation

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

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 array
  • def jStringPL: Json @?> JsonString — Retrieves a value only if the JSON value is a string
  • def 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
查看更多
兄弟一词,经得起流年.
3楼-- · 2020-05-26 10:55

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.

查看更多
贪生不怕死
4楼-- · 2020-05-26 11:05

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:

  • An ordinary Lens whose "field" is a Maybe type.
  • A Prism, as described by @J.Abrahamson.
  • A Traversal.

They all have their uses, but the first two are too restricted to include all cases, while Traversals are "too general". Of the three, only Traversals 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 0th 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 Traversals, 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 Traversals use.)

查看更多
登录 后发表回答