-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resuming widgets #17
Comments
Interesting! It's very illuminating to see concur's core mechanic from other people's perspectives, and to hear fresh ideas.
Well that's by design. If you are composing From a language consistency perspective - Since we are in a monad and continuations are hidden behind a lambda, it's not possible to say how any change in state would affect the child widgets, i.e. it's going to be very hard to maintain the state of the child in any meaningful form. For example, if the parent renders the counter for some states and does not render it for some states, then do what does it mean to maintain the count across those parent state changes? Applicative functors (selective or otherwise) are not appropriate because they don't allow plumbing the output of a previous widgets into a subsequent widget. Adding selectivity just allows us to write the equivalent of do
x <- someWidget
y <- anotherWidget x
... One quick solution, that I just thought of while composing this reply, is creating some sort of an applicative-ish combinator which nests things while semantically treating them as siblings, so updates to one do not affect the other. Imagine two combinators like this - <|$> :: Widget HTML a -> (Widget HTML a -> Widget HTML b) -> Widget HTML b
<$|> :: (Widget HTML a -> Widget HTML b) -> Widget HTML a -> Widget HTML b Both of them compose So then we could write the counter example as - counter :: forall a. Widget HTML a
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 = counter 0 <|$> counterContainer
where
counterContainer counterUI = do
newStr <- div [] $ [ counterUI, other str ]
counterContainer newStr Both these combinators can be easily written in a similar manner as the existing I'll try to add these soon and check if it works. |
I ended up pursuing a different direction, especially in the context of Replica. The problem really is just about how to best share state in the context of neverending widgets. So, having a
For example, to implement two counters sharing the same state, we could do:
The cool thing is that this allows for super easy state sharing among multiple Replica connections for basically free (i.e. open an app in two tabs and watch modifications in one tab being reflected in the other). The main drawback is that However, combinators like e.g.
Note:
What do you think? From what I understand you've already written bigger apps in Concur, so I'm really interested in hearing about what patterns work in your opinion. EDIT: Also, this kind of ties into your |
I don't understand how this solves the local state problem. In the Counter example, the (shared) state for both the counters is explicitly managed "above" the counter level, which is already easy in Concur. For example, it's trivial to write something like counter :: Int -> Widget HTML (Either Int r)
counter x = do
div [ onClick ] [ text (T.pack $ show x) ]
pure $ Left (x + 1)
counters = loopState 0 \x -> div []
[ counter x
, counter x
] Can you give an example of using |
For example:
Afaics that would be impossible with EDIT: i.e. imagine sharing some state between two completely unrelated components, each with their own steps, consisting of long-running or neverending widgets. |
Okay I see what you mean. The API looks very clean, but IMO semantically the API is a tad more magical than it should be. Specifically, the API controls both the input and output of the widget, but passing state "down" into a widget is not a problem with Concur. Also, if I understand the semantics correctly, as soon as an intermediate state value is emitted via What do you think of the following API instead - with :: forall a void. ∆ a -> a -> Widget HTML void
local :: forall a v r. a -> (∆ a -> Widget v r) -> Widget v r
With this API we can write your example like below. I think it makes the data flow more explicit. counter :: ∆ Int -> Int -> Widget HTML Unit
counter k x = do
void $ D.div [ P.onClick ] [ D.text $ show x ]
if x < 10
then with k $ x + 1
else pure unit
counterWithMessage :: forall a. ∆ Int -> Int -> Widget HTML a
counterWithMessage k init = do
counter k init
D.div [] [ D.text "Counter finished" ]
countersWithMessage :: forall a. Widget HTML a
countersWithMessage = local 0 \k -> D.div []
[ counterWithMessage k 0
, counterWithMessage k 0
] |
FYI - I added a simple implementation for |
@pkamenarsky On thinking about this more I have warmed up to the API you suggested, except for the implicit wiring with counter :: Wire (Widget HTML) Int -> Widget HTML Unit
counter wire = do
let x = wire.value
void $ D.button [ P.onClick ] [ D.text $ show x ]
if x < 10
then wire.send $ x + 1
else pure unit
counterWithMessage :: forall a. Wire (Widget HTML) Int -> Widget HTML a
counterWithMessage wire = do
counter wire
D.div [] [ D.text "Counter finished" ]
wireWidget :: forall a. Widget HTML a
wireWidget = local (Tuple 0 0) \wire -> D.div []
[ D.div' [D.text "This counter is independent of the other two "]
, counterWithMessage (mapWire L.first wire)
, D.div' [D.text "These two counters have the same state"]
, counterWithMessage (mapWire L.second wire)
, counterWithMessage (mapWire L.second wire)
] |
Ah, that's clever! However, I'm proposing something subtly different -
The general goal is to be able to easily compose long-running or neverending widgets with shared state.
EDIT: I think your
However, I'm hesitant to include this in the API, since it would encourage a more "stateful" style of programming, and I think |
Quick experience report: I've been doing Concur (well If I hadn't seen this issue I wouldn't have known what to do-- happily I was able to copy the For the sake of new users and Concur adoption, should we consider moving them or some alternative solution into the library? |
A few more thoughts: Making sure I understand the problemOne 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 solutionsImagine 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 Currently with The However, this isn't as descriptive as we could be, because gives the widget the power to use the An ideaWhat about parameterizing Then, for this example, the type of the unit designer would be This might be a horrible idea, but I thought I'd throw it out there in case it's interesting. |
I kinda agree with @ajnsit here in that concur-core should probably not provide functions and combinators for managing state in specific ways, I think it should only concern itself with widget composition (and their timelines). I believe it's possible to have a much nicer API for this problem which doesn't involve talking about state — one of the strongest core ideas or consequences of the concur model. This idea essentially uses these core combinators: -- | Fork the given 'Widget', allowing it to run in parallel with any other 'Widget'
-- while keeping its internal timeline/state closure. Widgets forked with this function
-- can be joined back again in another timeline with the 'join' function.
-- For external observers, a forked 'Widget' never finishes until it is joined with 'join'.
fork :: Widget ui a -> IO (Widget ui (Forked a))
-- | Erase the return type of a forked 'Widget', preventing it from being joined back in a timeline.
forget :: Widget ui (Forked a) -> Widget ui void
-- | Join a forked widget (i.e. a 'Widget' whose return type is @'Forked' a@) in the
-- current timeline, allowing waiting for its termination and inspecting its return value.
join :: Widget ui (Forked a) -> Widget ui a So for the problem described we could have: counter :: Int -> Widget HTML void
other :: Text -> Widget HTML Text
loop :: a -> (a -> Widget ui a) -> Widget ui void
container :: Text -> Widget HTML void
container str0 = do
forkedCounter <- liftIO $ fork $ counter 0
loop str0 \str ->
div [] [ forget forkedCounter, other str ] Which, as a diagram is something like this (sorry for the sloppy drawing): If we wanted countUntil100 :: Int -> Widget HTML Int
someComponentThatTakesALongTimeToFinish :: Text -> Widget HTML Text
container :: Text -> Widget HTML Int
container str = do
forkedCounter <- liftIO $ fork $ countUntil100 0
newStr <- div [] [ forget forkedCounter, someComponentThatTakesALongTimeToFinish str ]
oneHundred <- join forkedCounter
... Something like Please let me know what you @ajnsit and @pkamenarsky think of this. |
@arthurxavierx that's an interesting model! Though I'm not sure I understand the semantics. For example, this piece of code - newStr <- div [] [ forget forkedCounter, someComponentThatTakesALongTimeToFinish str ]
oneHundred <- join forkedCounter The |
@ajnsit I'd thought that, just as in the fork/join concurrency model, the counter would be able to end before If we want to race both processes such that the counter can end before the long running component, then the best approach would be just not forking any widget, right? countUntil100 :: Int -> Widget HTML Int
someComponentThatTakesALongTimeToFinish :: Text -> Widget HTML Text
container :: Text -> Widget HTML Int
container str = do
result <- div [] [ Left <$> countUntill100, Right <$> someComponentThatTakesALongTimeToFinish str ]
case result of
... |
Imagine the following scenario:
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:
one would have to turn the above into a state machine:
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:I.e.
orr
returns both the value of the endingWidget
, as well as all the continuations of the remainingWidget
s at that point. With this, we could rewrite the first example to: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-firingWidgets
. I think it should be possible to write something like this: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 opaqueWidget
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 ajoin
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 eachWidget
apipe
-like thing along withyield
andawait
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 makingWidget
a selectiveApplicative
, 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 theWidget
VDOM node (which would never change).However, although
SelectiveDo
might be implemented someday, until it isn't it's fairly cumbersome to program with selectiveApplicative
s. So that's off the table, at least for now.Do you have any thoughts on this?
The text was updated successfully, but these errors were encountered: