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

Suggestion for notification when data backing a lens changes #5

Closed metalone closed 7 years ago

metalone commented 7 years ago

I think it would be nice to get notified when the data backing a lens changes. I suppose the simplest method is to be able to attach a callback function to the lens. For example clojure atoms have the methods (add-watch atom key fn) (remove-watch atom key) where the callback function is fn [key atom old-value new-value]

Ultimately, I think anytime you are dealing with notifications things can get complicated. It might be best to have a means of delegating the notification task. There are considerations of pushing vs polling. There are concepts of having formulas based upon reactive inputs and then the formulas need to be reactive also. Then you get problems like batching changes and notifying on an animation frame. There can be consistency issues with the data.

The following is a story for the motivation of what I want. I have a GUI built with Python/Qt. I'm trying to figure out a better design for it. I have spent some time looking at React.js, Redux.js, Knockout.js, Reagent/Reframe.cljs, Trellis(py), Elm. I desire to borrow ideas from these tools, but it is taking me too long to figure out these tools, especially how they are implemented.

I like the idea of storing all the application state in a global atom. I like the broad idea of thinking about changes to this global atom as a fold() function. fold(reducer, app-state, events) I like the idea of the app-state data structure being a persistent data structure and there being a single mutable atom holding the app-state. I like the idea of one way data flow, where the GUI sends messages to the data store, and it is these messages that are queued up for the fold operation above.

I want GUI elements that can be composed into hierarchies and moved around. By GUI element, I mean a collection of widgets with some functionality.

I want the GUI to automatically and efficiently re-render when state that the GUI depends upon changes.

The thing I don't like about Reagent/Knockout/Trellis is that in order to get change notification I need to wrap every single piece of data into some kind of object. A ratom/observable/cell. I don't want my app-state composed of tons of little mutable objects so that I can get notified when their data changes. I want a single persistent app-state.

I want my GUI elements to depend on lenses/paths into the app-state. I want a GUI element to re-render when that data behind the lens/path changes.

I'm not certain, but with Elm, I think one needs to pass pieces of the global app-state down the tree hierarchy of GUI elements. That is app-state is refined further and further as it is passed down the tree. Leaf nodes see just what they need, but parent nodes have extra state.

I think I like the idea of each GUI element having read-only access to the entire global app-state through a set of paths or a lens.

I think I can get everything else working with some sort of lens changed notification.

ingolemo commented 7 years ago

The closest thing I can think of to what you're describing would be a lens that triggers a callback whenever it sets a focus to a new value. I'm not sure how useful that lens would be since it would be easier to trigger the callback manually, but you could make it like this:

from lenses import lens

def on_changed_lens(callback):
    '''A lens that does nothing, but as a side-effect it calls
    a callback when you try to set a different state.'''
    def setter(state, new_focus):
        if state != new_focus:
            callback(state, new_focus)
        return new_focus
    return lens().getter_setter_(lambda a: a, setter)

def item_changed_callback(old_item, new_item):
    key, old = old_item
    _, new = new_item
    print(key, 'changed from', repr(old), 'to', repr(new))

app_state = {
    'mode': 'Normal',
    'keys': ['h', 'j', 'k', 'l'],
    'colour': 'White',
}

change_watcher = on_changed_lens(item_changed_callback)

print('Starting with:', app_state)
app_state = lens(app_state).item_('mode').add_lens(change_watcher)[1].set('Insert')
app_state = lens(app_state).item_('keys').add_lens(change_watcher)[1][2].set('c')
print('Ending with:', app_state)

Output:

Starting with: {'mode': 'Normal', 'colour': 'White', 'keys': ['h', 'j', 'k', 'l']}
mode changed from 'Normal' to 'Insert'
keys changed from ['h', 'j', 'k', 'l'] to ['h', 'j', 'c', 'l']
Ending with: {'mode': 'Insert', 'colour': 'White', 'keys': ['h', 'j', 'c', 'l']}

Is that at least an approximation of what you want?