cyclejs: Incorrect usage of the term "pure"

I’ve recently been catching up with Cycle.js. The library has really improved since the last time I looked at 👍. I also watched the Egghead videos which was very enjoyable. One thing that caught my attention though is that the term “pure” seems to be used a bit too liberal in the documentation. For example in the Streams section the documentation states:

Streams become very useful when you transform them with the so-called operators, pure functions that create new Streams on top of existing ones.

It then shows an example using xstream’s fold. However, fold is not pure. In fact any stateful operator in xstream/RxJS/most/etc. is impure. The impurity is rather subtle but occurs because the result of fold depends not only on its arguments—but also on the time at which it is called.

We can see this with a simple example (JSBin here).

function main(sources) {
  const inc$ = sources.DOM.select('.inc').events('click').mapTo(+1);
  const refresh$ = sources.DOM.select('.ref').events('click').startWith(0);
  const lastSum$ = refresh$.map(_ => inc$.fold((x, y) => x + y, 0)).flatten();
  const vdom$ = lastSum$.map(count =>
    div([
      button('.ref', 'Refresh'),
      button('.inc', 'Increment'),
      p('Counter: ' + count)
    ])
  );
  return {DOM: vdom$};
}

In the example each time the “Refresh” button is pressed the displayed sum is reset to zero. But why? At all times is lastSum$ equal to an invocation inc$.fold((x, y) => x + y, 0). If fold was a pure function refresh$.map(_ => inc$.fold((x, y) => x + y, 0)).flatten() should be equal to inc$.fold((x, y) => x + y, 0). But this is not the case.

We can also try to make a simple refactor to the program:

    const inc$ = sources.DOM.select('.inc').events('click').mapTo(+1);
    const refresh$ = sources.DOM.select('.ref').events('click').startWith(0);
+   const sum$ = inc$.fold((x, y) => x + y, 0);
+   const lastSum$ = refresh$.map(_ => sum$).flatten();
-   const lastSum$ = refresh$.map(_ => inc$.fold((x, y) => x + y, 0)).flatten();
    const vdom$ = lastSum$.map(count =>
      div([

We are simply taking the sub expression refresh$.map(_ => sum$).flatten() and giving it a name. If fold was pure it would satisfy referential transparency and the refactor wouldn’t change the program. But it does. After this change the “Refresh” button does nothing. This, again, is because fold is impure.

Since any stateful combinator in xstream/RxJS is impure and any non-trivial Cycle-app will have to make use of them this means that most uses of “pure” in the documentation are incorrect or misleading. I think what the documentation should emphasize instead is that Cycle-apps are completely free from from side-effects and non-reactive code. These things are huge advantages in themselves.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 4
  • Comments: 23 (12 by maintainers)

Most upvoted comments

xstream docs don’t mention the word “pure” neither “referential transparent” at any moment.

Indeed, yes. That is why I opened the issue against Cycle.

If this issue is just about the particular stream library involved, we can of course add support for other stream libraries, like flyd, or Bacon, or Sodium.

As far as I know all the reactive stream libraries in JavaScript are impure—so you don’t get truly pure code from any of them.

I have probably not expressed this clearly enough, but I don’t think the impurity is the problem. Impurity vs purity is always a trade off and there are many good reasons to have an impure streams library. It can have a simple API without loosing power. All the pure FRP implementations I’ve seen either loose too much power (like Elm) or add significant complexity to usage of the API (using arrows or monads). One might argue that Cycle sits at a sweet spot where code is reactive and free from side effects but without requiring the complexity that would be needed to be pure FRP. It just shouldn’t advertise purity where it isn’t.

@Hypnosphi

Sorry I used wrong term. The previous stream is stopped (instead of reset) but like @Hypnosphi said, if the stream has other listeners then they of course continue consuming events (thus the stream is not really stopped).

No problem. Thank you for the explanation. I think I understand it now.

I’m not sure if I understood your second example right: what is the “correct” behaviour in that example?

There isn’t really any “correct” behavior. There wasn’t any “correct” behavior in the first example either. In fact in the first example the behavior that would be most intuitive to me is if the “Refresh” button did nothing. But the only thing I’m trying to show is that the API is impure by showing that it doesn’t satisfy referential transparency.

Referential transparency is a property of pure code that tells us that we can always replace a reference to a definition with the definition itself. Conversely we can also always replace an expression with a variable that is defined to be that expression. For instance we can easily see that 4 + 4 is the same as const n = 4; n + n but const n = Date.now(); n + n is not the same as Date.now() + Date.now(). This is because Date.now is impure and that fact severely limits the amount of refactorings that we can do with code that uses Date.now.

Referential transperancy is very useful both for understanding and for refactoring code. In general it just makes it easier to reason about how a program works. However the last example shows that by replacing inc1$ and inc2$ with their definitions we actually modify the behavior of the program. This is because fold in combination with flatten leads to impurity.

As the video posted by @Hypnosphi explains this is pretty fundamental about FRP. Some reactive APIs cannot be implemented in a pure way. Thus I believe there is no easy fix to the impurity and the Cycle documentation ought to not call things pure that aren’t pure. Doing that only misleads people.

Yeah there’s definitely something weird to be fixed. debug() presence does something weird too. But it’s good, I finally nailed down a small reproducible case. http://jsbin.com/hihuvuqupo/edit?html,js,console Will put up an issue on xstream repo.