Skip to content

Resuming widgets #17

@pkamenarsky

Description

@pkamenarsky

Imagine the following scenario:

counter x = do
  _ <- div [ onClick ]
    [ text $ T.pack (show x)
    ]

  counter (x + 1)

other :: T.Text -> Widget HTML T.Text
other str = do
  e <- div []
    [ input [ onInput, value str ]
    , text str
    ]

  pure $ targetValue $ target e

container str = do
  newStr <- div [] [ counter 0, other str ]
  container newStr

I.e. a composition of both neverending and non-recursive widgets. The problem is that every time other finishes, counter is going to lose its state.

To fix this, we could "ban" recursion (and thus neverending widgets) and explicitly thread arguments between parent and children components, essentially emulating Elm, but in a somewhat free-form way. However, disallowing recursion isn't even the worst thing; to fix state loss, instead of writing a widget like this:

workflow = do
   a <- step1
   b <- step2 a
   ...
   pure b

one would have to turn the above into a state machine:

workflow = do
  st <- get
  case st of
    Step1 -> do
      a <- step1
      put (Step2 a)
    Step2 a -> do
      b <- step2 a
      put (Result b)

To me, reifying time flow is the selling proposition of Concur and something no other UI paradigm offers, to my knowledge. Going back to explicit state machines in the spirit of React or Elm doesn't make much sense.

I've thought a bit about this but the solution I've come up with feels a bit off. Basically, we'd change the type of orr to:

orr :: [Widget v a] -> Widget v (a, [Maybe (Widget v a)]) -- specialised to Widget

I.e. orr returns both the value of the ending Widget, as well as all the continuations of the remaining Widgets at that point. With this, we could rewrite the first example to:

resume = flip fromMaybe

container str c = do
  (newStr, [c, _]) <- div [] [ resume c $ counter 0, other str ]
  container newStr c

But this does not seem ideal. It would be nice if we didn't have to modify orr for this, but then there would be no way to get hold of the continuations of the non-firing Widgets. I think it should be possible to write something like this:

reify :: Widget v a -> Widget v (a, [Maybe Widget v a])

which would return the result along with all the continuations of a Widget's children, but being able to break the encapsulation of the otherwise fully opaque Widget type that easily is probably a bad idea.

I've also thought about crazy stuff like actually calling all continuations after a Widget ends, effectively running the world in parallel and introducing a join combinator - which somehow collects the results from the different "parallel universes" - but that seems like it would be awfully inefficient and probably not even possible. Sounds cool though.

Maybe I'm overlooking something fairly obvious. I saw the Gen stuff in the Purescript repo and thought about making each Widget a pipe-like thing along with yield and await operators, so that outside state can be "pushed" into neverending widgets, but this wouldn't help if widgets can still finish and thus force their siblings to lose state.

I've also had the idea of ditching the Monad constraint altogether and making Widget a selective Applicative, which still allows for some control flow but is fully introspectable. This would bring the benefit of being able to collect every UI transition upfront (and maybe even precompute DOM diffs) but more importantly, of allowing us to attach the continuations directly to the Widget VDOM node (which would never change).

However, although SelectiveDo might be implemented someday, until it isn't it's fairly cumbersome to program with selective Applicatives. So that's off the table, at least for now.

Do you have any thoughts on this?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions