Lens is a library for Haskell that provides lenses, isomorphisms, folds, traversals, getters and setters, which exposes a uniform interface for querying and manipulating arbitrary structures, not unlike Java's accessor and mutator concepts.
Lenses (and other optics) allow us to separate describing how we want to access some data from what we want to do with it. It is important to distinguish between the abstract notion of a lens and the concrete implementation. Understanding abstractly makes programming with lens
much easier in the long run. There are many isomorphic representations of lenses so for this discussion we will avoid
any concrete implementation discussion and instead give a high-level overview of the concepts.
An important concept in understanding abstractly is the notion of focusing. Important optics focus on a specific part of a larger data structure without forgetting about the larger context. For example, the lens _1
focuses on the first
element of a tuple but doesn't forget about what was in the second field.
Once we have focus, we can then talk about which operations we are allowed to perform with a lens. Given a Lens s a
which when given a datatype of type s
focuses on a specific a
, we can either
a
by forgetting about the additional context ora
by providing a new valueThese correspond to the well-known get
and set
operations which are usually used to characterise a lens.
We can talk about other optics in a similar fashion.
Optic | Focuses on... |
---|---|
Lens | One part of a product |
Prism | One part of a sum |
Traversal | Zero or more parts of a data structure |
Isomorphism | ... |
Each optic focuses in a different way, as such, depending on which type of optic we have we can perform different operations.
What's more, we can compose any of the two optics we have so-far discussed in order to specify complex data accesses. The four types of optics we have discussed form a lattice, the result of composing two optics together is their upper bound.
For example, if we compose together a lens and a prism, we get a traversal. The reason for this is that by their (vertical) composition, we first focus on one part of a product and then on one part of a sum. The result being an optic which focuses on precisely zero or one parts of our data which is a special case of a traversal. (This is also sometimes called an affine traversal).
The reason for the popularity in Haskell is that there is a very succinct representation of optics. All optics are just functions of a certain form which can be composed together using function composition. This leads to a very light-weight embedding which makes it easy to integrate optics into your programs. Further to this, due to the particulars of the encoding, function composition also automatically computes the upper bound of two optics we compose. This means that we can reuse the same combinators for different optics without explicit casting.