ingolemo / python-lenses

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

Allow unbound lenses to modify, set, etc #8

Closed Daenyth closed 7 years ago

Daenyth commented 7 years ago

Proposed api along with a motivating example:

from lenses import lens
class Foo(NamedTuple):
    x: int
    y: int

x_plus1 = lens().x.modify(lambda x: x + 1)
y_0 = lens().y.set(0)
both = y_0.add_lens(x_plus1)
fs = [Foo(2, 2), Foo(3, 3)]
new_fs = map(both.run, fs)

Edit: actually the proposed api doesn't make a ton of sense, but I'd still like to be able to compose multiple setters together

ingolemo commented 7 years ago

Just to be clear, you would expect list(new_fs) to have the value [Foo(3, 0), Foo(4, 0)], right?

What I think you're asking here is for the unbound methods to be curried; e.g. for lens().set(value) to be equivalent to lambda state: lens().set(value, state=state). You could then do both = lambda state: y_0(x_plus1(state)), change both.run => both, and everything else would work. That wouldn't be hard to implement and it would make lenses play nicer with other functional libraries.

One of my worries is how confusing it might be to someone who forgets to provide a state. Expecting to get a new state and receiving a function instead is a lot less clear than the current behaviour of raising an exception.

Another worry is that there are already three different ways to provide state to a lens (lens(state), lens().bind(state), and lens().get(state=state)). It may be cleaner to remove the state keyword arg and the bind method and have users rely solely on currying for doing late-binding.

Daenyth commented 7 years ago

I agree about removing the keyword in favor of currying. And yes expected values would be as you say. The reason I might suggest an explicit run method is for clarity and explicitness vs call, because as you said they can forget

In fact I think it wouldn't hurt the api to remove bound lenses entirely in favor of the curried style, although I could be wrong - I'm still new to it

On Fri, May 19, 2017, 3:34 AM ingolemo notifications@github.com wrote:

Just to be clear, you would expect list(new_fs) to have the value [Foo(3, 0), Foo(4, 0)], right?

What I think you're asking here is for the unbound methods to be curried; e.g. for lens().set(value) to be equivalent to lambda state: lens().set(value, state=state). You could then do both = lambda state: y_0(x_plus1(state)), change both.run => both, and everything else would work. That wouldn't be hard to implement and it would make lenses play nicer with other functional libraries.

One of my worries is how confusing it might be to someone who forgets to provide a state. Expecting to get a new state and receiving a function instead is a lot less clear than the current behaviour of raising an exception.

Another worry is that there are already three different ways to provide state to a lens (lens(state), lens().bind(state), and lens().get(state=state)). It may be cleaner to remove the state keyword arg and the bind method and have users rely solely on currying for doing late-binding.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/ingolemo/python-lenses/issues/8#issuecomment-302631533, or mute the thread https://github.com/notifications/unsubscribe-auth/AAA5NOsdm91_dZ4ui3wYxSc5UXHf0Lalks5r7UYbgaJpZM4Nfz39 .

ingolemo commented 7 years ago

The latest commit (be2095e) has an implementation of this feature as I described. It was harder to implement than I'd expected, solely because mypy really didn't like the methods having different return types depending on the binding state of the Lens. Ended up splitting the bound and unbound lenses into separate classes, which changed the reprs and broke a few old doctests.

While I like the explicitness of run, it would mean adding an extra level to indirection around stuff for no other reason than as an extra hoop to jump through.

This code ought to do what you want:

x_plus1 = lens().x + 1
y_0 = lens().y.set(0)
both = lambda state: y_0(x_plus1(state))
fs = [Foo(2, 2), Foo(3, 3)]
new_fs = map(both, fs)

You may have a point with bound lenses...

Daenyth commented 7 years ago

Wow, thanks for the quick turnaround. This will be really helpful for me!