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

js-CSP compared with streams and events #40

Open
nmn opened this issue Feb 8, 2015 · 33 comments
Open

js-CSP compared with streams and events #40

nmn opened this issue Feb 8, 2015 · 33 comments

Comments

@nmn
Copy link
Contributor

nmn commented Feb 8, 2015

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

// stream is a stream of data.
var s1 = stream.map(fn1);
var s2 = stream.map(fn2);

// both s1, and s2, will be mapped from all the values fed into stream.
// The values are copied for every method called on stream.

However, the one-to-many data stream can be simulated with a helper function, and Dropping channels.

function toMany(ch){
  var channels = [];
  go(function*(){
    while(true){
      var val = yield take(ch);
      for(var i = 0; i < channels.length; i++){
        yield put(channels[i], val);
      }
    }
  });

  return function(){
    var ch = chan(buffers.dropping(0));
    channels.push(ch);
    return ch;
  }
}

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)

// ch is a channel where values are being put
var subscribe = toMany(ch);
var ch1 = subscribe();
var ch2 = subscribe();
// ch1 and ch2 now both have all the values being put onto ch.
// using methods from transducers, and toMany downstream again, you can simulate all the behaviour of streams.

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.

@zoomclub
Copy link

zoomclub commented Feb 8, 2015

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!

@zeroware
Copy link
Contributor

zeroware commented Feb 9, 2015

If you look at the code of js-csp you will see that there's already some function to do pub/sub with channels.

@zoomclub
Copy link

zoomclub commented Feb 9, 2015

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)

@nmn
Copy link
Contributor Author

nmn commented Feb 9, 2015

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

@ubolonton
Copy link
Contributor

@nmn

However, the one-to-many data stream can be simulated with a helper function, and Dropping channels.

The toMany example you gave would have worked with the old no-transducer version, not the latest one. This is a limitation of the current transducer integration https://github.com/ubolonton/js-csp/blob/42658bd7c82f9c0ba8fa932c90662067aedbb0ad/src/impl/channels.js#L63. We may need to revisit the alternative implementation discussed in #7 if dropping(0) turns out to be a common pattern.

these features should be exported as modifier functions rather than making channels with many forms.

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.

Buffers are another example where I think helper functions can be used.

I briefly thought about replacing buffers with reducing functions. Is that also what you have in mind?

@zoomclub

There is a great discussion going on here: arqex/curxor#1 (comment)

Thanks for the link. It's great.

I'll start finding more RxJS/Bacon examples to port.

@nmn
Copy link
Contributor Author

nmn commented Feb 11, 2015

@ubolonton

I am not I understand why my toMany example won't work with the latest version. As far as I can see the way buffers are implemented may be a problem. I don't have an answer yet, but I'm sure it can be worked around.

I briefly thought about replacing buffers with reducing functions. Is that also what you have in mind?

I think I have the same thing in mind. For clarity, here is what I mean:

// instead of 
var ch = chan(buffers(n));

// we should have something like
var ch = buffer(n, chan());

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:

function distribute(ch){
  var channels = [];
  go(function*(){
    var i = 0;
    while(true){
      var val = yield take(ch);
      yield put(channels[i], val);
      i = (i+1)%channels.length;
    }
  });

  return function(){
    var ch = chan();
    channels.push(ch);
    return ch;
  }
}

I can see this method being very useful for doing heavy computation on many web workers and sharing the load equally.

@zoomclub
Copy link

@ubolonton

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.
O = Operations like the ones I can make with JS-CSP
V = React and React Native
E = Input events from different providers, such as mouse, touch, myo, sensor, qwerty, etc

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)?

@ivan-kleshnin
Copy link

RxJS and BaconJS are amazing for doing async programming in a pre-generator world.

@nmn, did you meet this two articles?

http://potetm.github.io/2014/01/07/frp.html
http://potetm.github.io/2014/01/27/responsive-design-csp.html

That guy reimplemented David Nolen's CSP examples on Bacon + ClojureScript and got
very interesting results. He came to the opposite conclusion that it's hard to see the declared benefit of CSP over Rx (at least from the app code point of view),.

It would be great if you read them an tell your opionion.
At this moment very few people managed to understand both approaches and
are able to compare them.

@nmn
Copy link
Contributor Author

nmn commented May 22, 2015

@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:

  • FRP is a much higher-level abstraction and comes with many more goodies and features. It also comes with a centralized error handling. CSP on the other hand is a much lower level tool, and does very little.
  • For someone new to async programming, CSP is a much easier concept to learn, however, in most of the common cases, FRP is much more concise. With high level methods like flatMap, FRP libraries give a simple way to handle complex interactions. CSP gives you a lower-level primitive that can easily be used to accomplish similar tasks using good old loops and if constructs, but obviously that involves more code.
  • CSP has a default case of one sender and one receiver. FRP is more or a pub-sub model. So they are really well suited for different tasks. That said, CSP supports Mults and pub-sub as well, and FRP can be made to work in a more CSP way.

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.
CSP is smaller and simpler, but it comes with fewer batteries included. But it does come with a complete minimal toolset that can be used to accomplish everything that FRP can be used for, just with some extra code, or with helper libraries.

It's not completely correct, but I would say comparing FRP to CSP is like comparing Frameworks to Libraries.

@ivan-kleshnin
Copy link

@nmn, thank you! Very good summary and I can't find anything to argue.

@andershessellund
Copy link

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

@nmn
Copy link
Contributor Author

nmn commented Jan 26, 2016

@andershessellund That's a very important point to make.
Other than just back pressure, waiting on puts can have other benefits as well.

There's a reason CSP works extremely well for sync across threads.

@ivan-kleshnin
Copy link

@andershessellund, @nmn what do you think of graph-based stream solutions like flyd and Elm?

P.S.
Flyd seems to implement a hybrid approach close to "Arrowized FRP" library by classification of Evan Czaplicki.

  • Signals aren't connected to the world (by default).
  • Signal graphs aren't static (you can create new "signals").
  • Sync by default (event order is preserved).

Except the point

  • Signals can be finite.

@lucasmreis
Copy link

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 go, put and take, and achieve very complex results.

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 dispatch with a particular data structure. dispatch puts the data structure inside the actionsChannel. There's a go routine taking from that channel, that updates the state, and the screen re-renders.

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.

@ivan-kleshnin
Copy link

@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

anything dealing with asynchronous behavior that doesn't completely embrace generators natively is busted

which makes me really want to believe it. But I'm not sold yet.

@lucasmreis
Copy link

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.

@ivan-kleshnin
Copy link

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.

@nmn
Copy link
Contributor Author

nmn commented Jan 29, 2016

@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 async function* proposal, Observables can work with imperative loops etc. So saying CSP is less functional is really myopic observation to make.

(also, can we please stop using "not-as-functional" as a basis for comparison?)

@ivan-kleshnin
Copy link

@nmn you are right, this phrase is not correct.

@ivan-kleshnin
Copy link

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.

  1. FRP libs have to handle sync and async behavior in parallel (Zalgo!) to implement HO-stream operators like flatMap and merge. This requires huge imperative bookkeeping boilerplates.

  2. I'm not sure why, but FRP libs are mostly "thick". The code tend to be huge and obscure. Maybe that's because of stretching a plain event emitter idea to the point it does "everything in the world".
    Nevermind, tons of issues and bugs in appropriate repose say us something.

  3. FRP libs conflate abstract general-purpose code with app-level things like error-handling and stream completion.

  4. FRP libs reimplement all the standard FP toolkit violating DRY brutally.

  5. Most FRP libs provide multiple behavior patterns like Cold and Hot observables which are so different it's hard to even call them the "same thing". The term Observable with all that "extensions" becomes barely meaningful. It's not an emitter, not a stream in a classic sense. It's "something"...

  6. FRP libs tend to implement operators with ad-hoc imperative logic instead of composing them from other operators. They call it "performance" but it looks more like "premature optimization" to me. Or just bad API where you're implied to deal with a prebuilt set of "magic" operators.

  7. Backpressure. While you can apply buffering, lossy throttling, and sampling such solutions are more or less workarounds due to unidirectional nature of reactive streams.

  8. Diamond case or Glitches. Despite Andre's opinion I don't think of it as an "exagerrated" problem. It depends on the kind of the system you're modelling. While there are possible graph-based solutions to this problem I'm yet to see them in real libs.

  9. Bad defaults.

My rule of thumb for RxJS code is now:
Add .share() to every stream (prevent multiple subscriptions).
Add .shareReplay(1) to every stateful stream (keep initial value emitting).

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.

@lucasmreis
Copy link

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.

@ivan-kleshnin
Copy link

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

@danyx23
Copy link

danyx23 commented Feb 2, 2016

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.

@ivan-kleshnin
Copy link

@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? 😉

@zeroware
Copy link
Contributor

@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

@lucasmreis
Copy link

@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 :)

@ivan-kleshnin
Copy link

@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 x == y does not mean f(x) == f(y).
We already has language with broken equality rules (JS). I don't want one more.
(I do know why example above works like this).

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 core.async too literally. For example, Channel should block on take which immediately "sounds like Promise" but it's implemented as part of the Worker instead... Not that separation of concerns I expected to see in JS.

@zeroware
Copy link
Contributor

@ivan-kleshnin Clojurescript is not an option for me either. I wouldn't be using js-csp otherwise ;)
I was pointing at https://github.com/jcouyang/conjs because it's a vanilla JS library which is using clojurescript transpiled code on the inside like mori.js.

@nmn
Copy link
Contributor Author

nmn commented Feb 13, 2016

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.

@ivan-kleshnin
Copy link

Waves

RxJS

With 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 solution

Without 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 Code

let 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);

@lucasmreis
Copy link

I just came accross this library: https://github.com/bbarr/medium

I like this API better than the alternatives. And I particularly like the repeat and repeatTake idea.

@ivan-kleshnin
Copy link

@lucasmreis thanks! Looks interesting. I'll check it out.

@tiye
Copy link
Contributor

tiye commented Sep 9, 2016

@ivan-kleshnin in Clojure, [] is a vector and '() is a linked list. For vectors. New elements are conjed like being appended, while for linked lists, new elements are like being prepended. More often we use (cons x1 '(x2 x3)) to add new elements at the head. And '() is somehow lazy and yes it's strange.

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

No branches or pull requests

9 participants