zrho / purescript-optic-ui

PureScript UI framework based on lenses.
122 stars 10 forks source link

Regions with local generations #19

Closed zrho closed 8 years ago

zrho commented 8 years ago

The async problem turned out to be exceedingly annyoing to cope with. I've had many crutches in mind, like a 'mailbox' primitive that is stored in the state and renewed on each update, and so on. Ultimately, the problem with async updates is that the library can not determine which parts of the state are independent and thus can keep their handlers; thus the global generation number.

But what if we tell the library which parts are independent? I'm thinking of a function along the lines of region :: forall eff m v s. UI eff m v s s -> UI eff m v s s with which we can build a tree of regions in the application state that each carry their own local generation number. When a handler is invoked, the generation number of the region it is defined in, i.e. in which it has been retrieved via with, is increased; the (transitive) child regions are replaced by the regions generated on UI update for this part of the state.

This gives the user control over saying which parts of the state are independent and can be independently updated. Thus some handlers actually survive state updates in unrelated parts of the application. Of course, there is nothing that prevents the user to shoot himself in the foot by declaring overlapping regions, but using dimap irresponsibly can introduce the same kind of problems, so we already have that.

I'm trying to implement that now; I think this could solve (parts of) the async problem, at the cost of some inconvenience.

Edit: There is some undesired behaviour when removing something from a list: the list itself must have its own parent region, and once an element is removed, all child regions, i.e. all regions for e.g. the otherwise independent elements of the list, are invalidated...

zrho commented 8 years ago

I tried different implementations of this and it turned out to require a rather complex implementation which would break the spirit of simplicity of the library. I decided to go for 3f4030f022a44381bfa30985966712a6a05ec5db instead, which is a lot less involved.

FrigoEU commented 8 years ago

Hey Lukas,

I need more time to really understand how async/listen/raise is implemented and what the consequences are, but on first sight it looks purely superior to the current ref-based approach. Being able to get rid of the Eff in the Remote datatype is soo much easier. Great step in the right direction! I'll definitely start using this immediately.

Another solution I tried to get working, but failed to implement foreach/traversal, was defining the Handler as:

newtype Handler eff t s = Handler ((s -> t) -> Eff eff Unit)

In Run the handler would then always get the most current state passed in, avoiding the need to always have the most current state closed over in your handlers.

This would enable you to just write:

with \s h -> ui $ H.button [H.onClick \_ -> runAff (\e -> h (const $ Failed e )) 
                                                   (\r -> h (const $ Success r)) 
                                                   somethingAsync] 
                           (H.text "Click me")

It has other problems, but I was just wondering what you think about it?

zrho commented 8 years ago

The Profunctor, Strong and Choice parts indeed seem to work. With traversals however, it isn't only a problem of implementation: say that the traversed container changes, we associate updates to an index of a slot of the container and there still is an object at that index. Is it the same? Would we really want to make the update at that position? Say we make an async request for some entry in a list, and then delete this list element. When the async request returns, it will look at the index it came from and update it, overwriting an entirely unrelated element. Also two async computations could compete and you'd have to be very careful to check whether to apply updates in the handler to avoid nasty glitches. And finally, there would be two states flying around in a user interface definition: the current state as well as the updated state; you'd have to be careful not to confuse them.

I think that maybe it would be possible to do this, but I think that it is very easy to shoot yourself in the foot when doing something like this. Renewing handlers every generation gives you a way of saying: 'yes, I still do want this to happen'.

FrigoEU commented 8 years ago

Ok, thanks for the explanation. Understood and agreed :).