Introduction
For most users, the lenses library exports only one thing worth knowing
about; a lens
object:
>>> from lenses import lens
The lens
object is an instance of lenses.UnboundLens
. An unbound
lens represents a computation that you want to perform in order to access
your data from within some data-structure.
Here’s some simple data:
>>> data = [1, 2, 3]
Suppose that we wanted to access that 2
in the middle. Ordinarily in
python we would index into the list like so:
>>> data[1]
2
A bit of terminology; the data-structure that we are trying to pull
information out of (in this case; [1, 2, 3]
) is referred to as the
state. The piece of data inside the state that we are trying to access
(in this case; 2
) is called the focus. The lenses documentation uses
these terms consistently. For those who are unaware, the word “focus”
has an unusual plural; foci.
We can represent this pattern of access using lenses by doing the same
thing to lens
:
>>> getitem_one = lens[1]
The getitem_one
variable is now a lens object that knows how to retrieve
values from states by indexing them with a 1
. All lenses have readable
reprs, which means that you can always print a lens to see how it is
structured:
>>> getitem_one
UnboundLens(GetitemLens(1))
Now that we have a representation of our data access we can use it to
actually access our focus. We do this by calling the get
method on the
lens. The get
method returns a function that that does the equivalent
of indexing 1
. The returned function takes one argument — the state.
>>> getitem_one_getter = getitem_one.get()
>>> getitem_one_getter(data)
2
We ran through this code quite slowly for explanation purposes, but there’s no reason you can’t do all of this on one line without all those intermediate variables, if you find that more useful:
>>> lens[1].get()(data)
2
Now, the above code was an awful lot of work just to do the equivalent
of data[1]
. However, we can use this same lens to do other tasks. One
thing we can do is create a function that can set our focus to some
other value. We can do that with the set
method. The set
method
takes a single argument that is the new value you want to set and,
again, it returns a function that can do the task of setting.
>>> getitem_one_set_to_four = getitem_one.set(4)
>>> getitem_one_set_to_four(data)
[1, 4, 3]
It may seem like our getitem_one_set_to_four
function does the
equivalent of data[1] = 4
, but this is not quite true. The setter
function we produced is actually an immutable setter; it takes an
old state and produces a new state with the focus set to a different
value. The original state remains unchanged:
>>> data
[1, 2, 3]
Lenses are especially well suited to working with nested data structures. Here we have a two dimensional list:
>>> data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
To access that 8
we simply create a lens and “walk” to the focus the same
way we would without the lens:
>>> two_one_lens = lens[2][1]
>>> two_one_lens.get()(data)
8
>>> two_one_lens.set(10)(data)
[[1, 2, 3], [4, 5, 6], [7, 10, 9]]
Lenses are smart enough to only make copies of the parts of our state that we need to change. The third sublist is a different list from the one in the original because it has different contents, but the first and second sublists are reused to save time and memory:
>>> new_data = _
>>> data[0] is new_data[0]
True
>>> data[1] is new_data[1]
True
>>> data[2] is new_data[2]
False
Lenses support more than just lists. Any mutable python object that can
by copied with copy.copy
will work. Immutable objects need special
support, but support for any python object can be added so long as you
know how to construct a new version of that object with the appropriate
data changed. tuples
and namedtuples
are supported out of the box.
Here’s an example using a tuple:
>>> data = 1, 2, 3
>>> lens[1].get()(data)
2
>>> lens[1].set(4)(data)
(1, 4, 3)
Here’s a dictionary:
>>> data = {'hello': 'world'}
>>> lens['hello'].get()(data)
'world'
>>> lens['hello'].set('everyone')(data)
{'hello': 'everyone'}
So far we have only created lenses by indexing, but we can also access
attributes. Here we focus the contents
attribute of a custom Container
class:
>>> class Container(object):
... def __init__(self, contents):
... self.contents = contents
... def __repr__(self):
... return 'Container({!r})'.format(self.contents)
>>> data = Container(1)
>>> lens.contents.set(2)(data)
Container(2)
Of course, nesting all of these things also works. In this example we change a value in a dictionary, which is an attribute of our custom class, which is one of the elements in a tuple:
>>> data = (0, Container({'hello': 'world'}))
>>> lens[1].contents['hello'].set('everyone')(data)
(0, Container({'hello': 'everyone'}))
Getting and setting a focus inside a state is pretty neat. But most of the time, when you are accessing data, you want to set the new data based on the old value. You could get the value, do your computation, and the set the new value like this:
>>> data = [1, 2, 3]
>>> my_lens = lens[1]
>>> value = my_lens.get()(data)
>>> my_lens.set(value * 10)(data)
[1, 20, 3]
Fortunately, this kind of operation is so common that lenses support
it natively. If you have a function that you want to call on your focus
then you can do that with the modify
method:
>>> data = [1, -2, 3]
>>> lens[1].modify(abs)(data)
[1, 2, 3]
You can, of course, use a lambda
if you need a function on-demand:
>>> data = [1, 2, 3]
>>> lens[1].modify(lambda n: n * 10)(data)
[1, 20, 3]
Often times, the function that we want to call on our focus is actually
one of the focus’s methods. To call a method on the focus, we can use the
call
method. It takes a string with the name of the method to call.
>>> data = ['one', 'two', 'three']
>>> lens[1].call('upper')(data)
['one', 'TWO', 'three']
The method that you are calling must return the new focus that you want
to appear in the new state. Many methods work by mutating their data.
Such methods will not work the way you expect with call
:
>>> data = [1, [3, 4, 2], 5]
>>> lens[1].call('sort')(data)
[1, None, 5]
Furthermore, any mutation that method performs will surface in the original state:
>>> data
[1, [2, 3, 4], 5]
You can still call such methods safely by using lens’s call_mut
method.
The call_mut
method works by making a deep copy of the focus before
calling anything on it.
>>> data = [1, [3, 4, 2], 5]
>>> lens[1].call_mut('sort')(data)
[1, [2, 3, 4], 5]
If you can be sure that the method you want to call will only mutate
the focus itself and not any of its sub-data then you can pass a
shallow=True
keyword argument to call_mut
and it will only make a
shallow copy.
>>> data = [1, [3, 4, 2], 5]
>>> lens[1].call_mut('sort', shallow=True)(data)
[1, [2, 3, 4], 5]
You can pass extra arguments to both call
and call_mut
and they will
be forwarded on:
>>> data = [1, 2, 3]
>>> lens[1].call('__mul__', 10)(data)
[1, 20, 3]
Since wanting to call an object’s dunder methods is so common, lenses will also pass most operators through to the data they’re focused on. This can make using lenses in your code much more readable:
>>> data = [1, 2, 3]
>>> index_one_times_ten = lens[1] * 10
>>> index_one_times_ten(data)
[1, 20, 3]
The only operator that you can’t use in this way is &
(the bitwise and
operator, magic method __and__
). Lenses reserve this for something else.
If you wish to &
your focus, you can use the bitwise_and
method instead.
Lenses work best when you have to manipulate highly nested data structures that hold a great deal of state, such as when programming games:
>>> from collections import namedtuple
>>>
>>> GameState = namedtuple('GameState',
... 'current_world current_level worlds')
>>> World = namedtuple('World', 'theme levels')
>>> Level = namedtuple('Level', 'map enemies')
>>> Enemy = namedtuple('Enemy', 'x y')
>>>
>>> data = GameState(1, 2, {
... 1: World('grassland', {}),
... 2: World('desert', {
... 1: Level({}, {
... 'goomba1': Enemy(100, 45),
... 'goomba2': Enemy(130, 45),
... 'goomba3': Enemy(160, 45),
... }),
... }),
... })
>>>
>>> new_data = (lens.worlds[2].levels[1].enemies['goomba3'].x + 1)(data)
With the structure above, that last line of code produces a new
GameState
object where the third enemy on the first level of the
second world has been moved across by one pixel without any of the
objects in the original state being mutated. Without lenses this would
take a rather large amount of plumbing to achieve.