zrho / purescript-optic-ui

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

Async state updates are first-write-wins #15

Open ul opened 8 years ago

ul commented 8 years ago

See https://github.com/clayrat/opticui-elm-architecture/blob/master/src/6ViewerPair.purs#L46 for the concrete example. viewer has an async initializer, which makes AJAX request and then updates state. If I understand correctly how optic-ui works, when two or more viewers are rendered on the page then the fastest response updates application state and generation counter. Any other async handler which was run in the same generation fails to update state because of generation check https://github.com/zrho/purescript-optic-ui/blob/master/src/OpticUI/Run.purs#L53

Of course, if we will just remove generation check, situation will turn to last-write-wins, which is unacceptable too. We need to make async handlers composable.

zrho commented 8 years ago

Yup, that's problematic indeed. I've built the async component (https://github.com/zrho/purescript-optic-ui/blob/master/src/OpticUI/Components/Async.purs) as a crutch to sort of alleviate that pain; it works for me, but is not pretty to use. If I understand your code snippet correctly, you are trying to run the async op in a handler directly which is indeed first-write-wins. Have you already tried using the component (see e.g. https://github.com/zrho/purescript-optic-ui/blob/master/examples/ajax/src/Main.purs) and does the problem persist?

That being said, it would be great if there was a way to make this simpler without breaking the semantics. This is closely related to the "hidden" state discussed in the other issues; so if you happen to have an idea how to make this work more smoothely, I would be happy to hear!

ul commented 8 years ago

I can't figure out how to use async with onInitialized or another way to run it after loading without any user input. Would you mind elaborating on this?

One of the ideas from the top of my head about solving “hidden” state issue is to return from handler not new state, but some kind of setter.

FrigoEU commented 8 years ago

I started working on an implementation where you indeed pass a setter instead of the new state with runHandler, but the problem is that any other arguments that you closed over (eg: indexes when traversing over an array) can still get outdated between the start of the asynchroneous operation and it's resolution...

ul commented 8 years ago

I think that in general case library can't automatically decide how relevant and consistent is state update. Giving to user ability to provide setter in handler is the first step to give him control over such decisions. The second step is to make sure that setter, returned by handler, has access to both old and current application state and is able to decide what to do, emulating some kind of transactions:

пт, 4 дек. 2015 г. в 8:48, Simon Van Casteren notifications@github.com:

I started working on an implementation where you indeed pass a setter instead of the new state with runHandler, but the problem is that any other arguments that you closed over (eg: indexes when traversing over an array) can still get outdated between the start of the asynchroneous operation and it's resolution...

— Reply to this email directly or view it on GitHub https://github.com/zrho/purescript-optic-ui/issues/15#issuecomment-161901478 .

natefaubion commented 8 years ago

One solution might be to existentially bundle the state Ref and the getter/setter together with the Handler. This lets you keep the same signature for Handler but support more interesting options like getting and setting with the current state vs the closed-over state.

import Prelude
import Data.Exists
import Control.Monad.Eff
import Control.Monad.Eff.Ref

-- s' is the global state type that we existentially hide
newtype HandlerF eff s s' = HandlerF
  { getter :: s' -> s
  , setter :: s -> s'
  , ref :: Ref s'
  , run :: s' -> Eff eff Unit
  }

newtype Handler eff s = Handler (Exists (HandlerF eff s))

runHandler :: forall eff s. Handler eff s -> s -> Eff eff Unit
runHandler (Handler h) s = runExists run h where
  run :: forall s'. HandlerF eff s s' -> Eff eff Unit
  run (HandlerF handler) = handler.run (handler.setter s)

getState :: forall eff s. Handler eff s -> Eff (ref :: REF | eff) s
getState (Handler h) = runExists run h where
  run :: forall s'. HandlerF eff s s' -> Eff (ref :: REF | eff) s
  run (HandlerF handler) = handler.getter <$> readRef handler.ref

I don't know exactly how you'd pack the getters and setters, but that's the basic idea.