Here is a preliminary proposal of changes to make writing (complex) apps easier and make Flexx scale better. Everything is up for discussion, so don't hesitate to challenge it.
For context and discussion see #364. I am using a PR for this proposal, so that others can easily react in-line with the text. See the "files changed" section for what I think the docs for the new Model class could look like. Below I give a summary with argumentation.
The problems that I'd like to address with this proposal:
- A) Hard to predict flow of information in general.
- B) Need rather verbose code for simpler handlers and properties.
- C) Confusion of user on how the current
Model works. Py and JS is too "entangled".
- D) Flickering effect of properties (in Python) due to eventual synchronicity.
- E) Complex memory management (it was quite leaky until #380, also see #381).
- F) Its not trivial to write apps with centralized storage.
- G) No story for updating array data effectively (e.g. scientific purposes).
First off, in my view, the Elm architecture (Flux, Veux et al.) provides two main advantages/insights: having global state that can be observed/used in different parts of the application (helping with F); having a predictable flow of information, where certain user-interactions in the view can cause an "action" that starts a new flow of information (helping with A and D).
It took me a while to come up with an approach to include these ideas in Flexx, while also keeping things simple. I've considered everything from tiny changes to a complete overhaul of the event system. The latter is not what I propose, though some changes are pretty profound and not backward compatible.
Summary + argumentation
- Rename Model to Component, because it takes the role of both the M and V in MVC. The new component comes in two flavors:
PyComponent has all its logic in Python. It is instantiated in Python and can even be associated with multiple clients (e..g. for shared data in a chat app).
- No more need for the nested classes.
- The flow of information starts at actions, which mutate state, upon which there are reactions. Actions and reactions are really just methods on a Component class.
- State (properties) are readonly (makes code predictable) and can only be mutated from actions.
- Widgets should be more optional; it should be easy (and not feel like a hack) to use other means to realize the view of an app, e.g. using bootstrap.
- All components have a
root attribute that refers to the main application component. This can serve as a place to keep global state (a "store" in Veux/Flux terminology).
- Properties can be defined on a single line (saves lines of code).
- A standard (typed)
set_foo() action can be created automatically, because its so common by
foo = prop(settable=int).
- Introduce a "computed property", similar to automagic reactions mentioned below.
- Introduce an
array_prop to manage state in an array. Elements can be heterogeneous (list) or typed arrays (ctypes arrays in Python). Mutations can append/insert/remove items, making syncing/tracking such state very efficient.
- Perhaps introduce a
dict_prop which can similarly be mutated partially.
- Calling an action runs the action async (i.e. later), unless its called from another action. This makes it that the flow of information "starts" at the action.
- Actions can be called (i.e. invoked) from both Python and JS, as long as the args can be serialized. Think of
- Flexx handles one action at a time, and all reactions caused by the mutations done by the action are handled before moving on to the next action.
- Reactions are more or less what we call handlers now, using
- Reactions react to mutations to make the app reflect the new state.
- Reactions react to events, in most cases by invoking new actions.
- This still works:
@reaction(... connection strings ...)
- We also allow just
@reaction, and let Flexx figure out the dependencies automagically.
- Inline reactions:
Button(on_mouse_click=lambda ev: self.submit()). These will greatly help reduce code.
Renaming / restructuring
events subpackage made sense since Flexx is now very event-centric. The proposed changes make it more state-centric, and the name bugs me. Perhaps I'll just move it all to
- Maybe spin out
webruntime, so that we can just do
More motivation / argumentation
Similarity and differences w.r.t. Elm/Flux/Veux/Redux
Redux adopts a very "functional" approach to dealing with state, where reducers (i.e. actions) receive the state as an input argument, and return a new state. I don't doubt that this helps predictability, but it means having to make a copy of the whole state at each transition. This breaks down if we want to do something with data. It also does not work well with the OO style that Flexx uses.
A difference with Flux/Veux is that what they call "view", we call "reactions". One reason is that our reactions will often involve invoking actions on lower level components (e.g. widgets) rather than do DOM updates directly. So the scope of our reactions is somewhat broader, but the idea is very similar.
In most of the aforementioned frameworks there is a concept of a global/centralized state object, though each framework has another means to have some kind of modularity. In Flexx each component has its own state, and its up to the user to design the relationships in a sensible way, e.g. defining a widget's state as "local state" and using a common component, or the root component as "application state".
Debugging / testing
The clear flow of information also makes is possible/easier to create fancy debugging tools. E.g. see the list of pending actions, clicking a button to process the first one, see what mutations it causes, and which reactions follow from it. Similarly, it might help with (unit) testing.
Separation of Python and JS
My initial take (written down originally in this PR) was to allow instantiating components in JS, in which case they exist/operate only in JS. That would make it easier to write apps that clearly separate JS code from Py code (i.e. views from application logic). I now think that this is not sufficient.
By making a component either operate in JS or Python, the concerns of such a component are much more clear. The state that can still be observed from the other end, and the actions that can be invoked, provide an easy API to handle the Python-JS interactions.
A point of discussion was whether js-components could be instantiated from Python. If not, we'd need a way to somehow bind a Python and JS entry point together, which would greatly reduce the ease and number of places where JS and Python can interact. IMO the possibility to instantiate and use a js-component from Python does not break the JS/Py separation too much, since its still fully implemented in Python.
We'd need PyScript to support context managers and keyword args to allow instantiating components in JS in the same way as we do now.
I'd like a better story to manage arrays as state, without having to resend the whole array on each change, and making it easy for a view to update its local representation without a hard reset. Thinking of WebGL in particular, but also e.g. a list view with a lot of items. On top of this, one could implement "data sources" e.g. to manage image data.
To send mutations with data in the form of typed arrays we need a way to send binary messages. To keep things in order, this must happen over the same single websocket. So we should probably use a binary protocol instead.
tag: app tag: event type: discussion PR: declined