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 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?