Skip to content
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

A widget implementation experiment with Reactive.Threepenny #54

Open
duplode opened this issue Sep 13, 2013 · 7 comments
Open

A widget implementation experiment with Reactive.Threepenny #54

duplode opened this issue Sep 13, 2013 · 7 comments

Comments

@duplode
Copy link
Contributor

duplode commented Sep 13, 2013

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:

  • Basic usage: new returns a widget with no model, which is then connected to the rest of the application with either plugModel or its convenience applications simpleModel and userModel. All of the these three functions return the model (i.e. a Behavior whose value is displayed and possibly modified through the widget and which follows whatever promises the widget specification makes - e.g., for BoundedInput, that the value will be constrained to the bounds specified through new). In my application, I had no need of using the more general plugModel, as I do not need to filter or otherwise process the raw user event before using it to define the model. If you want an example of it in action, the implementation of simpleModel should give the general idea.

  • Given the decoupling of model and widget in implementations like this one, I reckon that the best description of their relation is not "the widget has a model which is used by the application", but "the widget is a tool used by the application to define a model". In support of that point of view, I mention an analogy born out of a discarded experiment. Imagine that we modified new so that it incorporated plugModel in order to return both the Behavior and the widget in a tuple. It would then be used like this...

    (bFoo, widgetFoo) <- new ..
    

    ... which looks a lot like this:

    (eFoo, fireFoo) <- newEvent
    

    Just as newEvent produces an Event and an IO action to trigger it, the combination of new and plugModel produces a Behavior and a IO tool to display and modify it; that is, the widget.

  • It is almost spooky how this style seems to ensure that the only convenient way of doing things is the clean, DRY one. Once I bought into the model-widget decoupling idea, everything fell into its right place, and many design improvements suddenly became obvious. That applies not only to the BoundedInput itself but also to the application code which uses it - for one, it made me realize that Behaviors are vastly superior to Events with respect to defining the semantically relevant parts of the event network (as opposed to mere glue code).

(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
Copy link
Owner

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

  • CurrencyConverter.hs -- Two input boxes representing the same amount of money in different currencies. Changing one of them will automatically change the other.
  • CRUD.hs
    • Two regular input boxes combined to yield a value (String,String).
    • A list box where changing the selection will set the aforementioned input fields.
  • 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.)

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
Copy link
Contributor Author

duplode commented Sep 17, 2013

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:

  • While Behaviors are the preferred way to specify the models which actually have meaning in an application, if the widgets are not in a one-to-one relation to the models there is no way to avoid handling event streams directly, as Behaviors do not compose in the necessary ways (i.e. an unionWith equivalent for Behaviors is semantically impossible). The currency converter example should be enough to illustrate that, as the two inputs there have, in principle, equal standing. I will probably try it next using the widgets to see exactly how it unfolds.
  • If plugModel is going to be split into a part which validates and returns user input and another which prepares and returns the model, and there may be processing/validation steps which should be applied both to program and user events (e.g. the enforcement of value boundaries, as currently done in the bounded input widget), then plugModel should handle user and program events separately; otherwise, user events might go through validation twice. The obvious way would be passing two Events and an initial value instead of a Behavior as arguments; there probably are more elegant solutions though. I do not believe such a change has any profound implications (the Behavior argument of plugModel is a bit of a ruse anyway, as it is not the final model), and as long as widget implementers maintain discipline (e.g. by only using the final model to affect the view) the three principles can be followed just as before.
    • Edit: I originally made some considerations about program events here; I removed them as attempting the currency converter example immediately shown the assumptions supporting them were flawed.

@HeinrichApfelmus
Copy link
Owner

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
Copy link
Contributor Author

duplode commented Sep 20, 2013

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. 😃

@HeinrichApfelmus
Copy link
Owner

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
Copy link
Contributor Author

duplode commented Oct 3, 2013

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
Copy link
Owner

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 😄 ), the library will force major collections after every event. Any problems should show up as elements disappearing seemingly randomly from the DOM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants