Resuming widgets
See original GitHub issueImagine 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 Widget
s 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 Applicative
s. So that’s off the table, at least for now.
Do you have any thoughts on this?
Issue Analytics
- State:
- Created 4 years ago
- Comments:13 (9 by maintainers)
Top GitHub Comments
A few more thoughts:
Making sure I understand the problem
One of the most enjoyable things about Concur to me is having local state at the leaves. Imagine in a strategy game, you’ve got things like open help tooltips, partially filled out forms (for things like setting what a base is producing), all that kind of stuff.
When doing this style of programming nothing above the leaf level can ever recurse on itself. If it does it will wipe out the local state of all its children.
So I think getting sharing of values like this right is going to be very important.
The current solutions
Imagine you’re making a level editor for a game. You want a form that can be displayed permanently on the screen for making new unit types. It has a “Submit” button to make a new unit type. When that’s hit you want to communicate it to the rest of the UI, but you also want to leave the state of the form alone, on the guess that the settings like
speed
,firepower
, etc might be similar for the next unit they create, and they will only want to tweak them instead of starting from scratch.Currently with
concur-core
the only way to get values out of a widget is to return them, so you’re basically forced into TEA:InternalState -> Widget html (Either InternalState NewUnitType)
. If you don’t report theInternalState
to your parent, it won’t have it on hand to re-initialize you once you return aNewUnitType
.The
Δ
strategy is definitely an improvement on this. You can make its typeΔ NewUnitType -> Widget html a
.However, this isn’t as descriptive as we could be, because gives the widget the power to use the
Δ
for both reading and writing, but we only want to use it for writing.An idea
What about parameterizing
Widget
itself? We could have aWidgetStream html read write return
and then dotype Widget html a = WidgetStream html Void Void a
.Then, for this example, the type of the unit designer would be
WidgetStream html Void NewUnitType a
.This might be a horrible idea, but I thought I’d throw it out there in case it’s interesting.
Quick experience report: I’ve been doing Concur (well
concur-replica
) programming for about a month now, and the scenario described in this issue was becoming a very serious problem.If I hadn’t seen this issue I wouldn’t have known what to do-- happily I was able to copy the
local
andwith
implementations. They’ve worked well so far.For the sake of new users and Concur adoption, should we consider moving them or some alternative solution into the library?