-
Notifications
You must be signed in to change notification settings - Fork 137
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
js-CSP compared with streams and events #40
Comments
Thanks for the enlightenment. I see a lot of RxJS being used and it would be great to define all the similarities and differences between the JS-CSP approach and the RxJS approach. Likely a article with code samples comparing these approaches would bring many up to speed on what is still a unclear concern. For instance people are asking the following and still have not heard of JS-CSP: http://stackoverflow.com/questions/28287251/what-is-rxjss-place-in-the-js-ecosystem-and-evolution Then there are courses like this: https://egghead.io/series/mastering-asynchronous-programming-the-end-of-the-loop?order=ASC Then there are sites like this: http://reactivex.io/ This comparison is needed in order for everyone to know what is best and narrow down the bewildering number of options available! |
If you look at the code of js-csp you will see that there's already some function to do pub/sub with channels. |
Word is that it comes down to a matter of taste, my taste buds like JS-CSP. There is a great discussion going on here: arqex/curxor#1 (comment) |
@zeroware The reason I wrote this issue, primarily, is to suggest that many of these features should be exported as modifier functions rather than making channels with many forms. I think it helps to show that channels are a low-level and simple concept that can be used in all sorts of situations with just a few helper functions. Buffers are another example where I think helper functions can be used. I want to know your thoughts on performance though. |
The
Can you elaborate on this? Currently they are already helper functions on top of channels. The stateful objects they return are for additional control of the underlying channels, not new types of channels themselves.
I briefly thought about replacing buffers with reducing functions. Is that also what you have in mind?
Thanks for the link. It's great. I'll start finding more RxJS/Bacon examples to port. |
I am not I understand why my
I think I have the same thing in mind. For clarity, here is what I mean:
Essentially, the chan method can become super simple as only a single type of channel and all other use cases are covered using helper methods. I like this approach mostly because with more use-cases in the future, it is always going to be possible to create a new helper function to help support a new use case, but adding more API to chan itself is going to be painful. For example, while the toMany function creates copies of values on a channel, we may also create a simple function to distribute values on many channels equally:
I can see this method being very useful for doing heavy computation on many web workers and sharing the load equally. |
Thanks, very much looking forward to the ported examples. This is very meaningful because each new example shows the versatility of CSP and also clues me into finding uses for it. Its looking like CSP can be used in many of my app features, so it is becoming very worth while taking the time to understand CSP. Also, it would be good to know where to mount CSP in React components. The whole integration with React is not brought to light yet, there must be a best approach? There days the rage is to reinvent almost everything so that the good APIs get obscured. IMHO after slicing and dicing my apps features I find it agrees most with the M.O.V.E. Pattern. Still others are promoting MVI or adding in FLUX, which I really think are unnecessary. http://cirw.in/blog/time-to-move-on I think we need to stop pleasing everyone (or you'll end up fluxy :) and just fill the void in the interaction area of React and JS-CSP looks like it is up to the task. For instance the react-events approach below is not enough. We need to know the best way to bring in events, then where to mount the JS-CSP processors in react components. Really, I think that JS-CSP should orient itself to all four pillars of the M.O.V.E. pattern and as a result have a best practice manifesto. https://github.com/jhudson8/react-events Here are the essential APIs behind M.O.V.E. for me: M = Firebase (it features immutable state and refs, which are cursors. What I really need to know most right now, is how to bring the E and the O together in the V. What is the best pattern for integrating JS-CSP in React (or within the greater context of app development)? |
@nmn, did you meet this two articles? http://potetm.github.io/2014/01/07/frp.html That guy reimplemented David Nolen's CSP examples on Bacon + ClojureScript and got It would be great if you read them an tell your opionion. |
@ivan-kleshnin I did read these two articles. They are indeed very good articles. I mostly agree with them, but it is important to put them in the context that the author has much more familiarity with Bacon. I have been looking this comparison a lot lately and I've come to the conclusion that things that I considered to be impossible with FRP are actually possible with some creative thinking. In the end, there is still not a clear winner but some key differences:
In the end, FRP is a higher-level much more complex and feature-rich construct which is concise. It has much more to learn, and is harder to understand at first. It's not completely correct, but I would say comparing FRP to CSP is like comparing Frameworks to Libraries. |
@nmn, thank you! Very good summary and I can't find anything to argue. |
An important difference between CSP and RxJS is that CSP implicitly supports back pressure. Putting to a channel does not complete until the channel is ready to accept more data. In RxJS you do not have any similarly easy means of handling this. https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/backpressure.md |
@andershessellund That's a very important point to make. There's a reason CSP works extremely well for sync across threads. |
@andershessellund, @nmn what do you think of graph-based stream solutions like flyd and Elm? P.S.
Except the point
|
I think that the spec of CSP leads to a much simpler API. We can work with channels basically with three or four functions, 90% of the time Compare that with Rx. The spec is simple too, but it leads to an enormous API. We need to have all those websites to figure out which operator to use in each case, like this one: http://reactivex.io/documentation/operators.html . That decision tree :) And, regarding React, I like to use CSP for top-down rendering and async actions. An example of how I use it in a top component: https://github.com/lucasmreis/happymenative/blob/master/js/view.js Every component that has an action that changes state just calls Works perfectly with small apps. For larger applications a little more fine tuning would be needed for performance purposes, but the idea would be the same. |
@lucasmreis CSP looks very imperative to me. I wonder if this is that final paradigm we should stop on. You have less operators, that's true, but you have more things to care: workers, channels, imperative loops. I saw a lot of code in CSP and it does not immediately strike me as "clearly superior". We shouldn't really limit ourself to RxJS vs CSP dichotomy because there are other approaches https://www.youtube.com/watch?v=Agu6jipKfYw and new ones continue to emerge. Smart people, like James Long find strong words like
which makes me really want to believe it. But I'm not sold yet. |
I agree with you - anything can be the best tool until a better one appears :) Regarding csp being imperative: the code inside a go routine is synchronous. You do one thing at a time, and each line of code waits for the last one to finish before running. The good part is that the whole go routine runs asynchronously, and it does not block the main thread if it's waiting for a line of code to evaluate. So, the code does not need to be imperative, but it's definitely synchronous, what makes it very simple to reason about. I think that's what behind James Long quote! Actors, for instance, are a subset of csp ("one buffered channel per go routine"), and Erlang is very functional :) And regarding Rx, I think it's an amazing tool, but even with the result code being less imperative than the csp code, I still think it's more complex for a lot of use cases. |
True. Cold vs Hot separation disturbs me because Hot is conceptually simplier yet requires additional code to "enable" it. But there are no ways to avoid this dichotomy wihout major paradigm shift. Finite streams make no sense without Cold behavior, for example. Anyway, Golang's and ClojureScript (core.async) success demonstrates that CSP is an interesting paradigm which deserves to be explored. |
@ivan-kleshnin I've heard this argument before that CSP is not functional or something. The fact is that when you use transducers with CSP it is just as functional, if not more functional than RX. That said, what makes CSP awesome is that you CAN use it with imperative loops etc, which is exactly why many people find it easier to learn. The fact is that with (also, can we please stop using "not-as-functional" as a basis for comparison?) |
@nmn you are right, this phrase is not correct. |
After I've spend tens of hours inspecting and comparing FRP solutions and even developt a simplified RxJS clone I tend to agree with @nmn, @andershessellund, @jlongster and other CSP advocates. Yes – It may be objectively said that most FRP implementations are inherently broken. Mostly because of incidental complexity but not limited to.
My rule of thumb for RxJS code is now: This is just too much of a code. To much care of technicalities instead of business logic. Strangely, most or all of this issues are inapplicable to Elm where Evan succeeded to forbid all sources of confusion and complexity common to reactive solutions (there are no higher-order signals, no finite signals, no custom error handling, etc). Now since I'm kinda dissatisfied with reactive streams I'm going to give CSP a deeper look. |
Very good compilation, nice work! And it's very interesting to see how Elm was successful implementing it. They selected just enough elements of Haskell et al to make it powerful and simple for this particular task. |
@lucasmreis, yeah, thanks for pushing me into right direction. I tried to grok Elm but as API was constantly changed, most examples were broken and I kinda abandoned it then. Maybe @danyx23 will answer us. For example, I'm curious how to implement "waves" of values in Elm having no nested signals. It's crucial for many animations. import {Observable} from "rx";
let s = Observable.interval(1000).flatMap(
Observable.for(
[0, 1, 2, 3, 4], (v) => Observable.of(v).delay(v * 50)
)
);
s.subscribe(console.log);
// 0-1-2-3-4-----0-1-2-3-4-----.... |
Interesting discussion! @ivan-kleshnin, the example you posted can't easily be done with Elm AFAIK. As you say, Elm doesn't have "higher order signals" (Signals of Signals) and therefore no operator like flatMap. This comes from the decision that Signals graphs are static, i.e. they don't begin or end. The solution I could think of for this example would have to emit at a high frequency and then drop values explicitly according to the current model state. (I.e. you would construct a list of tuples of points in time and values and then compare the passed time whenever the signal fires and see if you should emmit the next value). Interestingly, this solution feels more "imperative" (or maybe explicit is the better word?) even though elm is a purely functional language (the state is carried forward by a foldp in Elm and usually outside of your program). I find myself sometimes longing for some of the operators that RxJS has in Elm, because they are able to express some of these requirements in a very concise way. Another example that is surprisingly verbose in Elm is RxJs' auto suggest text box. But as Ivan wrote above in his list of concerns regarding RxJS, the added power of higher order signals comes at a significant cost in API surface, additional concepts etc. So for many problems, Elm took a very nice tradeof IMHO. It seems to me that these solutions all carry some of their history with them and looking for those maybe helps understand a lot of the design decisions better. The concept RxJS uses comes from the Reactive libraries in other languages, I think C# was first. There, threading is a big concern and one of the appeals the Reactive extensions have in those languages is explicit thread dispatching with the observeOn and subscribeOn methods. That is lost on JS as it is ATM of course. Channels are an interesting but very low level concept and I haven't ever really used them directly so far. Were they popularized by Go or where they mainstream before that? I think before using them in an app I would like to have some additional abstractions on top, and it would be interesting to see if those could avoid some of the problems the reactive extensions have. I imagine that the blocking nature of CSPs could be troublesome on it's own but I can't come up with a good example of where that may be problematic right now. |
@lucasmreis, what's your opinion on https://github.com/dvlsg/async-csp/ and other attempts to implement CSP on "standard" (more or less) JS toolkit. Can we nail this task without first class channel support in gen. controllers? Or "...there're tradeoffs..." as always? 😉 |
@ivan-kleshnin I'd prefer using clojurescript core.async with https://github.com/jcouyang/conjs That way you stick with the clojure implementation. reference : https://medium.com/@jcouyang/i-just-fork-mori-and-add-core-async-to-it-3cea689e9259#.km9dvv179 |
@ivan-kleshnin a couple of weeks ago I started a react native side project, and I decided to try 'async-csp'. Looks super clean, and I really like using async/await since every other JS library uses promises, and the final code feels more uniform. I also found it easier to explain channels to other js devs with this syntax. Here's how it looks like: https://github.com/lucasmreis/happymenative/blob/master/js/view.js But, as you can see, it's a super simple use case. I did not need to use any sliding buffers or alts, so I still don't know if it's worth it. And @zeroware, that library looks very promising! Clojurescript and Elm, each in it's particular way, are my two main sources of inspiration for front end programming :) |
@zeroware I used to be a fan of Clojure but I'm afraid I don't think it's a good language anymore. Just one example: (= '(1 2 3) [1 2 3]) ; true
(conj [1 2 3] 4) ; [1 2 3 4]
(conj '(1 2 3) 4) ; (4 1 2 3)
(= (conj [1 2 3] 4) (conj '(1 2 3) 4)) ; false In other words I see very few benefits ClojureScript gives over JS + Ramda. Same functional, vararg-oriented, untyped stuff with a funky syntax (I like LISP but not Clojure flavour of it). Java requirement is another big downpoint. Huge amount of obscurantism in community. Static typing denial is just the most prominent. I can continue but it makes no sense, IMO ClojureScript does not worth an ecosystem switch at all (Clojure vs Java were different). @lucasmreis cool! I'll give this library a kick then. JS-CSP seems to take Clojure version of |
@ivan-kleshnin Clojurescript is not an option for me either. I wouldn't be using js-csp otherwise ;) |
async-csp looks way better in my opinion. I'll try it out some time. But I've been burnt a bit by relying on JS-CSP. So, I'm going to wait and watch, and not use any of these libraries unless they become kind of popular. I've started using RXJS at work instead. |
WavesRxJSWith HO streams // RxJS ========================================================================
let v1 = Observable.interval(1000).flatMap(
Observable.from([0, 1, 2, 3, 4]).flatMap(v => Observable.of(v).delay(v * 50))
);
// equivalent to
let v2 = Observable.interval(1000).flatMap(
Observable.for([0, 1, 2, 3, 4], (v) => Observable.of(v).delay(v * 50))
);
v2.subscribe(console.log); CSP solutionWithout HO channels! Library code// CSP ========================================================================
async function timeout(timeMs) {
return new Promise(resolve => setTimeout(resolve, timeMs));
}
let interval = curry((valFn, timeMs) => {
let outChan = new Channel();
(async function () {
let c = 0;
while (true) {
await timeout(timeMs);
await outChan.put(valFn(c++));
}
})();
return outChan;
});
let throttle = curry((timeFn, inChan) => {
let outChan = new Channel();
(async function () {
while (true) {
let x = await inChan.take();
await outChan.put(x);
await timeout(timeFn(x));
}
})();
return outChan;
});
let map = curry((mapFn, inChan) => {
let outChan = new Channel();
(async function () {
while (true) {
let x = await inChan.take();
await outChan.put(mapFn(x));
}
})();
return outChan;
});
let consume = curry((fn, chan) => {
(async function () {
while (true) {
let x = await chan.take();
fn(x);
}
})();
}); App Codelet ch0 = interval(x => x, 100); // 0-1-2-3-4-5-6-7-8-9-10-...
let ch1 = map(x => x % 6, ch0); // 0-1-2-3-4-5-0-1-2-3-4-...
let ch2 = throttle(x => { // 0-1-2-3-4-5---0-1-2-3-4-5---
return x == 5 ? 500 : 0;
}, ch1);
consume(console.log, ch2); |
I just came accross this library: https://github.com/bbarr/medium I like this API better than the alternatives. And I particularly like the |
@lucasmreis thanks! Looks interesting. I'll check it out. |
@ivan-kleshnin in Clojure, |
@jlongster wrote a great article introducing js-CSP, and does a short comparison with promises. I think a more fair comparison would be to streams such as RxJS and Bacon.js.
This, issue will also play into Multiplexing, mixing, publishing/subscribing .
The major difference I see between channels and streams other than generator functions and API is:
One-To-Many Streams
Streams act like events and broadcast their values. As a result you can attach many listeners to the same stream of data. Channels, act as value bridges. One value put on one end, is only received by one receiver.
However, the one-to-many data stream can be simulated with a helper function, and Dropping channels.
Now, this can be used in exactly the same way that streams from RxJS and Bacon can be used.
(it would be better to use a set to hold channels rather than an array in an ES6 world)
This, I think shows the power and flexibility of channels. RxJS and BaconJS are amazing for doing async programming in a pre-generator world. But Channels are a lower-level construct that can be used to be used in the same ways, but also in other ways.
With a few helper methods, channels can be used to achieve the same semantics of promises or of streams, or a one receiver semantic that is unique.
Note: this function like other functions, still needs work to account for closed channels.
One downside of the one-to-many semantic of streams is that it is hard to send a message upstream telling the origin to stop emitting data.
The text was updated successfully, but these errors were encountered: