-
-
Notifications
You must be signed in to change notification settings - Fork 421
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
Cycle.js coding style puzzle: sink-HTTP-source, or just wrapping ajax promise to a metastream and then flatMap? #289
Comments
HTTP is not just in the computer. It crosses the network and talks to other computers. Drivers are for external communication, and network is really just that.
Yes.
Yes. That said, I do understand your point of view that it's not ergonomic to use the Stream I/O API for req/res. I totally get that. A monadic API is better (don't worry about the jargon, the point here is that there is theoretical foundation behind these issues). We are discussing in these issues about alternative HTTP driver APIs: |
@staltz thanks for the clear reply. Actually I have digged for hours into the previous issues you mentioned, and here are my thoughts: I totally agree with you that, if we hold the side effects standards, then all I/O operations have to go to drivers (even your new plans about the monadic API are still putting HTTP in drivers, not main(), right? ), and the main() function (and component functions) must be pure, which makes testing very easy. I think the testability is the primarily (if not the only) benefit by doing so. I understand you hold this standard quite firmly, so in #204 you are not interested in putting state data into drivers. And I agree with you on this point. But I have some further ideas that, not only state data, but also HTTP, websocket, localStorage... all these things may be moved out of drivers, into main(), if we can loose the side effects standard. In other words, drivers should only deal with human I/O through DOM, and all other I/O should be inside main(). I can name three main benefits of doing that:
Let me elaborate it. Firstly, I do feel some subtle incompatibility inside the philosophy of cycle.js. Sometimes the cycle means the cycle between logic and side effects, sometimes it means the cycle between computer and human users. So the line between app and external world is always vague. You mentioned:
OK, so database on the remote server is external, and localStorage on the client itself is still external. So what about client-side in-memory database (like client-side minimongo of meteor)? In fact I am doing a project that will make use of this sort of in-memory database on client-side. This database uses an async API (callback or promise) to fetch data from both localstorage and remote server(by ajax or socket.io), combining and syncing the two sources. Of course I don't need to write a driver for this in-memory database, because as you said, this is within my own client-side app. But, in fact the API of the in-memory database is exactly the same with that of the localStorage database and that of remote database I am using. I designed all the three databases. All of them are part of my app and my logic. Nothing external. And I don't see why I have to put the other two inside drivers and leave the in-memory one in main(), especially when I don't care the "logic - effect" division and can mock a database to do testing. Secondly, I feel the "logic-effect" standard is also somehow contradictory with your MVI pattern. Let's see, if the DOM is the only source passed from driver to main(), and the vtree$ is the only sink passed from main() to driver, then the main() function does fit the MVI pattern: you extract user intent (dom events) from source, process it into model, and then construct a virtual DOM view object to return. But with HTTP and other I/O stuffs mixed in the sources, the MVI pattern is kind of broken. "Intent" by its name means user operation to the DOM, not the HTTP or websocket response, but at the top part (supposingly the intent part) of main() we have to do HTTP response processing. And "view" by its name means the manipulation of the vdom, not the HTTP request, but at the bottom part of main() we have to return not only vdom, but also HTTP request etc. So when we do HTTP req and res through driver loops, in main() we see two parallel lines, one for DOM's 'MVI, one for HTTP. Of couse it's not a big deal, but traditionally, fetching data from server and posting data back to server according to users click events are always treated as the "model" part. Lastly, just as you mentioned here:
What I learn from this statement is that, unlike DOM source events which are quite diverse and unpredictable and hence be better to use the driver loop to handle, the HTTP request-response pairs are just traditional 1-to-1 callbacks or promises that are very easy to be transformed into flatMapped streams. So if we give up the HTTP driver loop and just embed the HTTP operations into main(), we won't break the data flow graph -- actually we are making the data flow graph even clearer, like: userClick$ --(map)--> HTTPRequest --(flatMap)--> HTTPResponse --(map)--> VDOM$ cycle.js's current way, by dividing the http req and res into two parts, actually makes this data flow a little bit hard to handle. The websocket push event stream, if used, can be similarly handled in main() without breaking the data flow graph, say, can be merged into HTTPResponse stream and then mapped to VDOM$. That being said, I agree that this is basically a "philosophical" or "architect" issue rather than technical. Both ways could deliver what developers require when building large apps. And the current way of course has its merits: testability, driver as the only place to do supscription...etc. The only thing I want to say is, there is still some vagueness and incompatibility inside cycle.js's design philosophy. We must make decisions and make them clear. Anyway, thank you for the wonderful design! |
sorry I got some network connection issue here and wrongly posted the above comment multi times just now. all cleared. |
And after reading #272, I do more prefer the design that there is no universal HTTP driver: each component makes its own HTTPRequest$ and handles its own HTTPResponse$ by flatMap; we can still put multiple response$ together by merge/combineLastest etc. if we need to; and so there's no isolation issue then. I don't see much benefit from having the potential to do pre- and post-processing for the HTTP I/O -- components can do that by their own. And I do hate filtering HTTP responses again and again. We do need a universal DOM driver because there is only one DOM root there, and all components' vdom$ have to be put together finnally to render the real one DOM; but we possibly don't need a universal HTTP driver because unlike DOM, HTTP is not a bus, not by its nature public. |
Ok, I understand your point of view.
The line isn't that vague because side effects are essentially "changing something in the external world". There is always a correlation between "doing real-world changes" and the concept of "side effect". I think the more sensible distinction is
HTTP is QA, localStorage is QA, etc. But DOM Driver is RT and also Web Socket is RT. Notice that the side effect doesn't need to be related to the user in order to be RT, because the Web Socket is not user interaction. Stream I/O (what you see in Cycle.js) is appropriate for RT. Monadic I/O or traditional impure I/O (what you want) is appropriate for QA.
So, if you believe this is a good idea and you don't care about losing pure testability for the HTTP effects, then go ahead and make the HTTP requests inside main(). You are acknowledging that main() is now not pure anymore, but doing so doesn't require new tools to be built in Cycle.js.
There is nothing incompatible really. MVI is meant for DOM concerns only, it's not meant for every effect concern. Check this component for instance where there is an MVI is an onion-architecture, but the more effects you add, you'll have to add more pre and post processing functions before Model. E.g. Intent is pre and View is post, for the DOM. For other concerns you build other pre and post functions as required, but the Model can take inputs from multiple concerns.
That doesn't mean that the traditional way is good. Model should be just about pure logic, because that's a good idea for the "onion-like" features of the architecture. Here is a small sample of what I'm talking about: https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html |
@staltz thank you for your patience. Yes now I realize my previous proposal is actually reducing Cycle.js into merely a wrapper of RxJS with a v-dom plugin. That's clearly not what you want. Cycle.js is more about a beautiful programming paradigm. |
@staltz when programming logic requires at a certain point of data flow diagram a transformation that needs to do a request (QA type) to an outside (HTTP), then a requirement to pass request into sink and wait for reply from a source will complicate (nuke?) a nice data flow diagram. If transformation function does HTTP request, is it automatically impure? It is a pure function, if QA was a read. It just happen to transcend boundaries, that's all. But, it is a pure function. If use of HTTP inside of a function makes testing a bit more complex, may be it is better to transfer complexity to testing, away from programs' structural wiring diagram. Test phase may have unittest stage, integration at different levels stages, etc. But, if original program's structure is simpler, may be, there will be less tests to attend? May be, this visualization, this help in seeing what program does is a baby in the water. @beeplin a circuitry of observables, with sinks and sources of RT events, is already that beautiful programing paradigm, visual, simple to grasp, maintainable in a long run. And yes, v-dom is a mere plugin on both sinks and sources sides for some RT events. And may be, further purity requirements are not as essential? About purity. An observable that accumulates incoming values has an internal state! Some of fundamental Rx elements have state! So, purity questions can be not particularly pure. Secondary note. In electrical diagrams, components with electric or magnetic capacity are called reactive (direct translation from ru, that is). What if you have to deal with a diagram that connects to a battery? Think of that battery as big chunk of state, data, etc. Somehow, electrical diagrams are used with complex elements in them -- no problem. The problem arises only when a diagram is unnecessarily complex. |
@staltz have you seen these people who say that Promises will be dropped in favour of observables? They are mixing QA and RT styles! Promise is a mere function call that happens to complete on a next tick. In langs with blocking, call blocks. When someone in a blocking lang starts to use promise/future it is done for performance reasons. Observable, on another hand, is a source of events. Function call <-> QA |
@3n-mb accessing anything outside your program (even accessing non-const variables in your program, or getting the current time) makes your function impure |
@jvanbruegge that's right, the deeper we go along purity argument, the more difficult it is to see the worth preserving it. When I look at a diagram, generated by cycle's devtool, I can see that some incoming event from dom, like click, starts an activation in a wiring of observables. RT (real-time) event drives that activation, event starts it. This is a uni-directional data flow (good). HTTP, or QA, or Request-Reply is a bi-directional single time interaction. Bi-directional has no place neither among uni-directional sinks, at the bottom of a data-flow diagram, nor among uni-directional sources, at the top of a data-flow diagram. What if we say the following. Folks, there may be times when in a certain section of your data-flow a request is needed, somewhere just a read, somewhere a write. Make those places smaller, and mark them as impure. In tiny examples of cycle.js, state is contained in little collectors. But it is a trivial example of state in a diagram. What if domain requires keeping a bigger state? This hybrid route hasn't been explored, yet. In some talk, Alan Kay was asked about functional programing, and the answer was simple: "Show me function in physical world, cause I cann't. Yet, I can show you lots of objects." This is a good observation. And it may not be a stretch to suggest that an evolution must've attuned our brains to recognize objects, inferring their state, etc. Let's recall What are we doing here? We have a Turing (state?) machine on one hand, higher primates brains on another, and all of programming languages, tricks, and beautiful paradigms in between. And we want to arrange pieces so that those brains a more efficient at creating programs. Pushing functional purity down their throats is not in my job description. And a hybrid approach is helpful here. Let folks see and appreciate the difference between pure and impure circuitry literally displayed by cycle.js dev tool. How can I highlight more, that this auto-magic visual show of what program is, done by cycle's dev tool, is a discovery in itself. |
Purity is a good thing, it's not something forced onto us, we chose it. It makes it easoer to reason about your code. You can be sure the function does not mess with the rest of the program. You can easily reuse it, because it does not interfere with anything in the reat of your app. We also dont have to limit us in any way, just because this is how the real world works like this. We dont have that limitation inside a computer. This is why the cycle architecture is great. As JS is not a functional language, so we have at some point to use imperative, side-effectful code. With cycle we can collect all those side effects in a single place (the driver). This way the rest of your code stays pure. |
@jvanbruegge unfortunately, when I introduce cycle.js into my team, they start to feel forced into the purity, cause when they open material on the net, this thing splashes into the face. And now they have no choice, but to follow my lead into cycles. It would be easy, if we explicitly talk about having some impure circuits, yet, easily grokable, thanks to visualization done by cycle dev tool, the whole pure/impure story stops being a boogeyman mystery. There is no good reason to hide state into driver. There is a good reason to keep it in a separate circuit, and, here is a diagram, as an example, thank you very much, kinda thing. It will also settle all of questions about deforming point http request into driver cyclic dance. |
Cycle is a functional, reactive framework. If you never used FP or FRP, you should make yourself familiar with those concepts. |
Here is this language: |
@jvanbruegge this is what cycle.js today. Notice how I talk about visual dev tool all the time. Its a mix that is powerful. If we do not try to hand inescapably needed state as a visualizible circuit, we won't have a chance to discover patterns, now visual in a diagram, that will tame state beast. Let's exercise logical purity here. If we push anything non-trivial into driver, it still becomes a circuit, it is half a circle, but on the outside (!), thus invisible to our wonderful cycle dev tool. |
Noone is hiding state in a driver btw, in the simple case you fold it somewhere inside your app. For more complex cases like onionify you wrap your app, leaving it pure. |
@jvanbruegge we always have to learn something. May be, with all do-pure requirements there even will be parity in a cognitive effort for both cycle and angular (as you brought it up). |
@jvanbruegge onionify -- cool! Does it have event that says that state has been safely saved on a server? No. Why should it. It is a general tool. One may need to have ones own state circuit. But, prevailing wisdom, you know, tells that circuits should stay pure, instead of saying that if these common patterns, like onionify, are not enough, go on, do a little impure circuit yourself, and here is a visual tool for you to debug this state handling. |
@jvanbruegge don't get me wrong, I get @staltz 's analogy with horses and cars made in a talk. I just happen to question edges that look a little sharper than needed for a smooth introduction of cycles to a 14-year old. There are 14-year olds that fly planes, you know. Activity that doesn't require pure math thinking. Just practical. |
In the traditional RxJS programming, when doing Http request, we just use
fromCallback
orfromPromise
to wrap a new stream, then map the request stream with it, building a meta-stream, and thenflatMap
it.But cycle.js recommends to put the Http thing out of main() and into the HTTP driver, just like what we do with DOM.
I understand the drivers mean 'outside world' or 'human user'and main() means 'computer'. But, yes, DOM events are from human users, while HTTP should still be on the 'computer' side.
So what's the point to put HTTP operations into drivers? Is it just because Http operations introduce side effects? Does that mean main() is not just 'computer' bu also pure, and all side effects must go to drivers, no matter they are from humans or from HTTP data fetching?
The text was updated successfully, but these errors were encountered: