ingolemo / python-lenses

A python lens library for manipulating deeply nested immutable structures
GNU General Public License v3.0
310 stars 19 forks source link

Help request: Turn Each() back into a whole-sequence lens? #26

Closed jjlee closed 3 years ago

jjlee commented 4 years ago

Hi

I'm trying to get my head around how to compose lenses / optics. Here's what I'm stuck on:


from lenses import lens

class Parcel(NamedTuple):
    sku: str
    size: int

class Line:
    def __init__(self, parcels):
        self.parcels = parcels

    skus = lens.parcels.Each().sku

    def print_first_sku(self):
        # This prints what I want ("abc")
        print(self.skus.collect()[0])
        # I would like to write something like this instead (but skus is not
        # defined correctly for this to work):
        print(self.skus[0].get())

def test():
    line = Line([Parcel("abc", 1), Parcel("bcd", 2)])
    line.print_first_sku()

if __name__ == "__main__":
    test()

I see why the second print does print "a" -- the [0] effectively indexes into the list items, not the list, and .get() is defined to return the first item in the resulting list of characters -- but I don't see how to define "skus" so that I can use it like self.skus[0].get() and get "abc" back.

Any clues? Am I approaching this the wrong way?

ingolemo commented 4 years ago

I believe what you're looking for is something the haskell lens implementation calls partsOf. This does not currently exist in this library, but I've been meaning to add it so I've just pushed a commit (020ac50) that implements this feature. Your example should now work with something like print(self.skus.Parts()[0].get()).

Note that this method is pretty deep-magic and should be avoided where possible because it's very easy to write code using it that violates lenses' internal assumptions. For this specific task you should go ahead and use the conceptually much simpler print(self.skus.collect()[0]).

jjlee commented 4 years ago

Thanks!

Can you educate me re in what sense it's easy to violate internal assumptions? For example: Is that mostly an implementation issue or a fundamental one? What sorts of assumptions are these and what kind of usage might violate them?

Your code looks elegantly simple-but-brain-exploding in the usual manner of this sort of fancy functional programming to me :-)

My code here was intended only to reasonably minimally demonstrate the issue as I understood it, and in reality my code is of course more complicated. I'm not sure how it will go, but if I recall I hoped to use lenses like this one to set values also. My desire to use it this way was in order to have some separation between the different pieces of data and code involved (in the example, by exposing only skus instead of parcels to code that needs to know only about that).

ingolemo commented 4 years ago

I shouldn't have been so vague; sorry.

The issue is that when you use Parts to set or otherwise modify your state you need to make sure that you maintain the length of the list. This is fundamental in the sense that if you don't do this then the optic is technically breaking the lens laws. Lenses have "laws" that they're supposed to follow in order to have predictable behaviour. This library doesn't emphasise them strongly anywhere in the docs because it's not a thing Python programmers typically care about, but if you're familiar with Haskell you'll know how laws work.

In practice, if you lengthen the parts list then this library will just ignore the extra items on the end, but if the list is too short then you'll get a gnarly exception. This behaviour is implementation specific and could change at any time. To wit; Haskell's partsOf actually has different behaviour here.

Example:

# setup
from lenses import lens
state = [[1, 2], [3, 4]]
l = lens.Each().Each().Parts()

# good
print(l.set([5, 6, 7, 8])(state))
print(l.call_mut_reverse()(state))

# bad
print(l.set([5, 6, 7, 8, 9])(state))
print(l.call_mut_append(9)(state))
l.set([5, 6, 7])(state)
l.call_mut_pop()(state)

In your specific example, because you're immediately indexing into the list that you create (.Parts()[0]) any setter function you apply to this lens won't have direct access to the list and so there's no way that it can change it's length.