Closed yarray closed 9 years ago
Concepts to be thought:
Thoughts:
State is History (part of Stream)
e.g. for system S = on|off -> S,
Data is State
e.g. select(index) = select(data[index]) = do('select', index) = select_
Action is alphabet (possible events) for input Stream
You can organize Actions (with Data and State and their patches) to create input Stream, but cannot transform it.
Event is alphabet for output Stream
You can transform the output Stream, but it's not created by you. Listening Event is equal to set up fmap, a specific way to transform the Stream.
Questions:
"is" means that only one the two concepts on both sides should be chosen, or if combined the boundary to orchestra them can be changed.
The conceptual model should be able to describe:
For simplicity we can offer only two types of control flow in traditional sense:
2 can be transformed to promise or RP. If all side effects of f(…, g) are in g, f is also can be transformed to an FRP component
UI's complexity resides in the fact that it is the center of 4 aspects: Dom IO and program IO, e.g:
A toggle button may:
Regarding Actions as input and Events as output, the most confusing part happens in following scenario:
PlusOne:
var onPlused = function() {};
// Event
function listenPlused(handler) {
onPlused = handler;
}
// Action
function plus(num) {
onPlused(num + 1);
}
Printer:
// Action
function print(num) {
console.log(num);
}
// no Event since it's end of the flow
If we bind as listenPlused(print)
then it's clear. However, suppose we only want to print the number when it's greater than 5, we may write:
listenPlused(function(num) {
if (num > 5) {
print(num);
}
});
Here Action (input) and Event (output) are not combined directly. We can define a new concept as "binder" or "adapter". However, there are some questions: Is it also a module? If it is, what's its input and output? It's obvious that we can migrate logic between "adapter" and our Action/Event module, so what's the boundary?
The problem is that we are inconsistent here. In fact the "adapter" can be transformed to standard module as:
var onGreaterThanFive = function() {};
// Event
function listenGreaterThanFive(handler) {
onGreaterThanFive = handler;
}
// Action
function putNum(num) {
if (num > 5) {
onGreaterThanFive(num);
}
}
The problem is: is it worthy to strictly obey the Action/Event abstraction if we use this style to do flow fontrol?
With Action and Event, a module is equal to a stream transformation in RP. However, RP is more data centric while Action/Event are more operation centric. Also, how input becomes output is expressed with stream operators in RP, but with normal imperative control structures in Action/Events. The two conceptual models seem to always raise some incompatibilities. Need to think over again.
There is a confusing case when thinking about DOM + UI component. It looks like a variation of Action/Event model. Use Toggler as an example:
function Toggler(button /* DOM */ ) {
this.on = true;
this.onToggled = () => {};
this.button.addEventListener('click', () => {
this.on = !this.on;
this.onToggled(this.on);
});
}
// Event
Toggler.prototype.listenToggled = handler => {
this.onToggled = handler;
}
There is no Action. Input is a DOM element and it can be regarded as a collection of Events. So it seems that it's an Event/Event component. But that's not concise.
The key point is Toggler without DOM makes no sense (What is Toggler control without underlying DOM is). You cannot use it separately. Given a object with a 'click' event, the code works, but it makes no sense and the concept is solid.
Now suppose we change the code to Action/Event:
function Toggler() {
this.on = true;
this.onToggled = () => {};
}
// Action
Toggler.prototype.toggle = () => this.on = !this.on
// Event
Toggler.prototype.listenToggled = handler => {
this.onToggled = handler;
}
// trivial binding code
var t = new Toggler();
// for DOM
this.button.addEventListener('click', t.toggle);
// for other event
x.listenYEvent(t.toggle);
Now we can find that Toggler is meaningful WITHOUT DOM. It is a abstract toggler now and can be combined with both DOM click events and other events without any conceptual confliction
So the options of conceptual models here are:
Blue boxes are concepts from the Action/Event model, while green ones are concepts from RP model. It can be seen that the black box logic resides in components for the Action/Event model, while it resides in binding part for the RP model. This part of logic in RP model is more clear but may be limited, since it uses stream operators instead of normal code.
We do not introduce non-trivial binding for Action/Event model. It will offer no advantages but may cause confusion as stated in the 5th comment.
There are also other conclusions here:
Explain concepts:
State
State is the history, including past data and events in FRP State is directly modeled in A/E
Data
Data is a special state, only differentiate itself from other states in concept
Data binding is a transform from data changes to DOM transitions, or, is a transform from data to some kinds of state (virtual DOM), which can be in turn flushed to DOM changes (render in React)
FRP itself can only support the second type till "some kinds of state", but we can combine it with other mechanisms.
"Not a component" in previous comments is not exact, it should be rather expressed as "interface coupled" versus "no coupling". Example:
Version 1:
function A(b) {
return doSomething(b.B());
}
Version 2:
function A(value) {
return doSomething(value);
// binder
A(b.B());
Version 1 is interface coupled with b, while version 2 is not coupled with anything, all composition logic resides in binder.
A possible way to combine two models is to use Action/Event on the top level, and implement it inside using FRP. It requires a little hack but is totally doable, an example with Bacon.js is:
function actionStream() {
var bus = new Bacon.Bus();
var f = function(data) {
bus.push(data);
};
f.stream = bus;
return f;
}
function createPluser() {
var plused = () => {};
var push = actionStream();
// frp inside
push.stream
.bufferWithCount(2).map(l => l[0] + l[1])
.onValue(a => plused(a)); // output to Event
return {
push,
onPlused: handler => plused = handler
};
}
var pluser = createPluser();
pluser.onPlused(res => console.log(res));
pluser.push(1);
pluser.push(2);
Rethink FRP: If we treat the stream as data stream rather than event stream, then the binding CAN be a part of component, or can be "no coupling". The structure in comment 8 changes to:
where semantics of Events are trimmed out of stream, and how to explain the data depends on each side of binders.
Another note on UI's complexity: interfaces for both shared Data/State and DOM are two way flow, and probably with side effect. It is just imperative programming without ANY restrictions!
Possible solution:
Concepts:
Options are things that can be changed on a UI control, while even if they are not changed the main functionality can still be accomplished.
Container can be well understood as an Action/Event component. Its problem in concept is discussed in comment 7. For each type of UI control conceptual models:
Structure of DOM is a fact and can be equally expressed as an emmet hierarchy string. Different CSS and JS use different sub hierarchy as interface.
DOM generation is like dynamically change data schema, which is quite error prone. How to deal with it?
Sometimes Data is a little superior abstraction than general State. Think about list with detail, while an item can be marked as "important", which will in turn represented by color in both sides.
Mutable data with events passed in is something like a Container.
State in DOM is hard to parse and full of noise. So a better choice is to keep another state, and sync (part of) it with (part of) DOM state, as in the case of data binding (view model vs. DOM state).
All cases where self-created stateful members vs. passed in stateful members can be discussed like the discussion on DOM in comment 4. Generated members are hard to be shared with others since its interface tend to be unstable.
Multipart controls are trivial, which need only some way to direct flows (Action <-> Event or bound common data). The complexity may happen if the flows are all two way.
Parent controls are just those of which Container interfaces are top part of the real DOM. Things get harder if DOM generation involves.
Challenge:
A list page with every element containing a button "edit". The editing page can be a modal, a expaned div in the same page, or another page, or even another website. How can we design the interface to be flexible? Several options:
Problems:
Best way to solve comment 18 should be solution 1 or just setting the button as a link. Other methods suffer from unclear control flow. For problem 3, editButtonClicked can be used as the event name.
1 can be solved. We can inject a redirection to the event, and different handlers can handle both cases. It reminds us the importance of routing design.
This thread produces enough insights till now. Remaining are decisions, the most important concerns are:
Rough answers now:
Libraries used:
Bacon for FRP.
Additional notes:
Think about the "ideal" UI engine for us, including:
What parts should a UI engine contain? What are the best ways to do that?
The conclusion should contain a conceptual model. The solving of this question should learn from other UI frameworks and can explain them with the final model, to justify their suitability for us.
Other UI frameworks/libraries