HeinrichApfelmus / threepenny-gui

GUI framework that uses the web browser as a display.
https://heinrichapfelmus.github.io/threepenny-gui/
Other
439 stars 77 forks source link

A widget implementation experiment with Reactive.Threepenny #54

Open duplode opened 11 years ago

duplode commented 11 years ago

I have just finished a rewrite of the bounded input widget of Stunts Cartography with the explicit goal of making it conform to the three principles proposed by Apfelmus in this blog post. I am pleased with the result - it does everything I need, and both widget and application code are better than before. You can check the widget implementation and how the rest of the application uses it (look for the BI. import qualifiers). The rewrite was hugely instructive, and there are several interesting little points to be made; in order not to bore you with a(n even longer) wall of text, however, I will stick to the essentials for now, and wait for your comments:

(This issue is a follow-up of sorts to #40 and #49. It is related to, but distinct from and possibly complementary to, #53.)

HeinrichApfelmus commented 10 years ago

Intriguing! I find the idea of plugModel receiving a Behavior and spitting one out again very interesting, though I'm not entirely sure whether this is really a good answer.

To give us a conceptual framework in which to talk about widget design, I have now uploaded the conceptual part of the widget design guide. (The "Implementation" part is unfinished and precisely what I'd like to find out.) The three principles are mentioned as well, in a form that supersedes the discussion in my old blog post. I don't think that I've hit their final form yet, though.

One of the main ideas is that the model always pushes into the view. This is important for bidirectional data-flow. In particular, the function userModel of your bounded input widget works fine as long as only the user edits it. However, what happens when you, say, switch to a different map and the program has to set the boxes to new values? A similar thing happens with your ratio widgets, where you use plugModel to incorporate data from both the model and the user.

I found the following examples to be very instructive

Concerning the plugModel function, I think it's important to split it into two parts: one where you can inject a model into the widget and one where you get user input out. (At the moment, this would only be possible with dynamic event switching in the FRP library, though.)

duplode commented 10 years ago

To give us a conceptual framework in which to talk about widget design, I have now uploaded the conceptual part of the widget design guide.

It is shaping up nicely! Meanwhile, I am trying to push the style of implementation described above to see how much it takes for it to break down. Thus far I managed to implement enough of a list box to believe your CRUD example would be feasible. The interface is rather different (the "database" Behavior is defined separately and then passed as an extra argument to both userValueChange and plugModel); that doesn't worry me too much, however, as there is probably no sensible way to have an uniform initialization interface. Here is the relevant gist.

In particular, the function userModel of your bounded input widget works fine as long as only the user edits it. However, what happens when you, say, switch to a different map and the program has to set the boxes to new values?

By choosing userModel over plugModel you are specifying that such a thing will not happen. In Stunts Cartography, userModel is only employed for parameters which are never set by the program. In fact, it is just a shortcut;

initialValue `userModel` widget

is the same as

initialValue `stepper` (userValueChange widget) >>= plugModel widget

Any program events would be incorporated to the Behavior which feeds plugModel.

A validated input widget -- Currently trying to understand that one. The idea is that the box will be highlighted in red if the user enters a wrong value, and only correct values propagate to the model. However, it seems that the model needs control over when to display the red "invalid" border if it wants to set the text boxes to model-defined values. (I'm thinking of replacing the input boxes in the CRUD example with these more fancy input boxes.)

Clarification: what should happen when the program sets the text boxes to an "invalid" value? I see three possibilities:

  1. The value is discarded (it reaches neither the view nor the model); or
  2. Only the view is updated (red border included); or
  3. The value is accepted (the view is updated - with no red border - as well as the model).

The second option would allow us to get away with handling user and program events in the same way (as is done in my bounded input). With the third option, the problem can be sidestepped by sinking into the border colour only when the input is focused. That would work as long as the program never sets the text box while the user is editing (or that you don't mind having the border highlighted in such a case). Finally, handling the user and program event streams separately (e.g. as distinct arguments to plugModel) makes even the first option possible, but at the cost of running afoul of Principle One. (Edit: if the model becomes something like Behavior (Bool, a), with the flag indicating that the user tried to set an invalid value, the principle would not be violated, or at least the letter of it. That raises other interesting questions, though - for instance, do we really want that piece of information to be part of the model which will be shared with the rest of the application?)

Concerning the plugModel function, I think it's important to split it into two parts: one where you can inject a model into the widget and one where you get user input out. (At the moment, this would only be possible with dynamic event switching in the FRP library, though.)

I didn't play with dynamic event switching enough to actually get it, but I guess that by the above you mean actually changing the user event field in the widget so that it incorporates whatever validation plugModel specifies? That would be indeed very useful, and reminds me of two related points:

HeinrichApfelmus commented 10 years ago

Concerning the validated input widget, I now think that Behavior (Bool, a) (or equivalently a pair of Behavior Bool and Behavior a) is indeed the right way to go.

Whenever the user inputs an invalid value, the question is: when will the program override the "this input is invalid" message and replace the value with a valid one? When the element loses focus? When the user clicks on a button? There is no canonical choice, so this information needs to be part of the model.

This is similar to how the selection of a list box needs to be part of the model.


Concerning a general widget implementation style, I am beginning to think that the CRUD.hs example got it mostly right: a widget is simply a function that maps input behaviors (model) to output events/behaviors (controller) and also returns a DOM element for actual display (view). Example: reactiveListDisplay.

In particular, we don't need to define a new data type for each widget. In fact, any data type we have defined so far was a record type, whose main purpose is to give convenient names to the components of a tuple. In the end, we do want convenient names, but Haskell's record system is currently not up to par, and I think we may want to look at some alternatives.

The only trouble with the style from the CRUD example is that it relies on recursion, which Reactive.Threepenny currently doesn't support. My next step will be to rectify this. Unfortunately, this will be an intrusive change, as I will have to remove most occurrences of the IO monad and replace them with a UI monad. For that, I will have to finish finish and merge my garbage collection branch first. In short, this will take some time to complete.

There is also the issue of "backwards compatibility" with the traditional imperative style, but I think that any Behavior argument can be converted into a WriteAttr. The only serious issue is that the traditional style assumes that every property has a meaningful default value.

duplode commented 10 years ago

In particular, we don't need to define a new data type for each widget. In fact, any data type we have defined so far was a record type, whose main purpose is to give convenient names to the components of a tuple. In the end, we do want convenient names, but Haskell's record system is currently not up to par, and I think we may want to look at some alternatives.

There seem to be opposing demands to be balanced here. For simple widgets (say, a checkbox), it would be very convenient to be able to pick any old Element, style it however you want and then use a widget function to associate it to a model. On the other hand, for complex widgets (specially ones with multiple elements) I sense it would be desirable to hide some of the complexity from the widget users by using an abstract type.

The only trouble with the style from the CRUD example is that it relies on recursion, which Reactive.Threepenny currently doesn't support. My next step will be to rectify this. Unfortunately, this will be an intrusive change, as I will have to remove most occurrences of the IO monad and replace them with a UI monad.

A little painful, but necessary - the boilerplate to plug two widgets bidirectionally without recursion is quite annoying. According to this plan, will both reactive and non-reactive code (e.g. element creation) move to the new monad or will it be more like reactive-banana-threepenny, with separate UI do blocks inside IO ones?

For that, I will have to finish finish and merge my garbage collection branch first.

Looking forward for that too, as it will make us able to implement cool stuff in good conscience. :smiley:

HeinrichApfelmus commented 10 years ago

According to this plan, will both reactive and non-reactive code (e.g. element creation) move to the new monad or will it be more like reactive-banana-threepenny, with separate UI do blocks inside IO ones?

Yes, this time, reactive and non-reactive code will share the same monad, which hopefully means less boilerplate.

duplode commented 10 years ago

Ooh, so now we have both garbage collection and recursion! I will start converting Stunts Cartography to the soon-to-be 0.4, and will report on any interesting findings, including those directly related to this issue.

HeinrichApfelmus commented 10 years ago

Ooh, so now we have both garbage collection and recursion! I will start converting Stunts Cartography to the soon-to-be 0.4, and will report on any interesting findings, including those directly related to this issue.

Garbage collection should be stable now, but it can't hurt to test it more extensively. If you compile the library with the -frebug (cabal) or the -DREBUG (ghci) flag (that's an "r", not a "d" as first letter :smile: ), the library will force major collections after every event. Any problems should show up as elements disappearing seemingly randomly from the DOM.