Composing Lenses

If we have two lenses, we can join them together using the & operator. Joining lenses means that the second lens is placed “inside” of the first so that the focus of the first lens is fed into the second one as its state:

>>> from lenses import lens
>>> index_zero = lens[0]
>>> index_one = lens[1]
>>> get_zero_then_one = (index_zero & index_one).get()
>>> get_zero_then_one([[2, 3], [4, 5]])
3
>>> get_one_then_zero = (index_one & index_zero).get()
>>> get_one_then_zero([[2, 3], [4, 5]])
4

When we call a & b, b must be an unbound lens and the resulting lens will be bound to the same object as a, if any.

It is important to note that doing two operations on two different lenses and then composing them is the equivalent to doing those two operations on the same lens:

>>> lens[0][1]
UnboundLens(GetitemLens(0) & GetitemLens(1))
>>> lens[0] & lens[1]
UnboundLens(GetitemLens(0) & GetitemLens(1))

The first is actually implemented in terms of the second, internally.

When we need to do more than two operations on the same lens we will often refer to this as “composing” two lenses even though the & operator is nowhere in sight.

Binding

The lenses library also exports a bind function:

>>> from lenses import lens, bind

The bind function takes a single argument — a state — and it will return a BoundLens object that has been bound to that state.

>>> bind([1, 2, 3])
BoundLens([1, 2, 3], TrivialIso())

A bound lens is almost exactly like an unbound lens. It has almost all the same methods and they work in almost exactly the same way. The major difference is that those methods that would normally return a function expecting us to pass a state will instead act immediately:

>>> bind([1, 2, 3])[1].get()
2

Here, the get method is acting on the state that the lens was bound to originally.

The methods that are affected are get, set, modify, call, and call_mut. All of the operators are also affected.

>>> bind([1, 2, 3])[1].set(4)
[1, 4, 3]
>>> bind([1, 2, 3])[1].modify(str)
[1, '2', 3]
>>> bind([1, 255, 3])[1].call('bit_length')
[1, 8, 3]
>>> bind([1, [4, 2, 3], 5])[1].call_mut('sort')
[1, [2, 3, 4], 5]
>>> bind([1, 2, 3])[1] + 10
[1, 12, 3]
>>> bind([1, 2, 3])[1] * 10
[1, 20, 3]

Descriptors

The main place where we would use a bound lens is as part of a descriptor.

When you set an unbound lens as a class attribute and you access that attribute from an instance, you will get a bound lens that has been bound to that instance. This allows you to conveniently store and access lenses that are likely to be used with particular classes as attributes of those classes. Attribute access is much more readable than requiring the user of a class to construct a lens themselves.

Here we have a vector class that stores its data in a private _coords attribute, but allows access to parts of that data through x and y attributes.

>>> class Vector(object):
...     def __init__(self, x, y):
...         self._coords = [x, y]
...     def __repr__(self):
...         return 'Vector({0!r}, {1!r})'.format(*self._coords)
...     x = lens._coords[0]
...     y = lens._coords[1]
...
>>> my_vector = Vector(1, 2)
>>> my_vector.x.set(3)
Vector(3, 2)

Here Vector.x and Vector.y are unbound lenses, but my_vector.x and my_vector.y are both bound lenses that are bound to my_vector. A lens used in this way is similar to python’s property decorator, except that the api is more powerful and the setter acts immutably.

If you ever end up focusing an object with a sublens as one of its attributes, lenses are smart enough to follow that sublens to its focus.

>>> data = [Vector(1, 2), Vector(3, 4)]
>>> lens[1].y.set(5)(data)
[Vector(1, 2), Vector(3, 5)]