kefirjs / kefir

A Reactive Programming library for JavaScript
https://kefirjs.github.io/kefir/
MIT License
1.87k stars 97 forks source link

question: canonical way to get "difference" of a stream #186

Closed boneskull closed 8 years ago

boneskull commented 8 years ago

Say I have a stream foos which uses filter():

var bars = foos.filter(value => value === 'bar')
  .onValue(doSomething);

I want everything in foos which is not in bars (like _.difference()).

Naively, you could write:

var notBars = foo.filter(value => value !== 'bar')
  .onValue(doSomethingElse);

I thought I might be able to use diff() for this, but the documentation makes it sound like it's not applicable.

rpominov commented 8 years ago

So you want something like the following?

var foos = ...some stream...
var bars = ...some stream...

var notBars = Kefir.difference(foos, bars)
rvikmanis commented 8 years ago

Something like lodash's partition could do the job in a single statement.

let [bars, notBars] = foos.partition(v => v === "bar")

And we can improve on the concept by allowing more than one predicate function:

let [bars, foobars, rest] = foos.partition(
  v => v === "bar",
  v => v === "foobar"
) 

(analogous to if-then-else having multiple else-if sections)

rpominov commented 8 years ago

Hm, this is interesting. Although I tend not to add helpers like that if it's just one line more without helper and doesn't needed very often:

const bars = foos.filter(v => v === "bar")
const notBars = foos.filter(v => v !== "bar")

Yet not sure this is what @boneskull wants, because this is exactly the same code as in original comment, and it should work.

pixelpicosean commented 8 years ago

Use a false filter after a true one may cause problems, take a look at this:

let doAction;
const playerState = Kefir.stream(e => doAction = e)
  .scan((_, act) => `is_${act}`);

const isWalking = playerState.filter(v => v === "is_walk");
const isNotWalking = playerState.filter(v => v !== "is_walk");

isWalking.onValue(() => {
  emitter.emit('run');
  console.log('player is running');
});
isNotWalking.onValue(() => {
  emitter.emit('walk');
  console.log('player is walking');
});

You're going to get a maximum call stack error, but it's just a small example that shows these two streams are not updated the same time. One will always update first and cause problems if you subscribe to both of them. My solution is to delay the second one but the result looks strange and not logical ;)

iofjuupasli commented 8 years ago

For more complex predicate you can use:

function complement(pred) { return function(){ return !pred.apply(null, arguments) } }

or inline

Kefir.filter(pred)
Kefir.filter((...args) => !pred(...args))

to avoid code duplication

@pixelpicosean but it's because you actually have infinite loop without any asynchronous calls which will break callstack. But partition method shouldn't be asynchronous, so you'll need delay anyway

partition seems totally redundant if you use complement. As far as kefir doesn't implement method sugars which isn't must have. (but maybe I miss something, and there is functional advantage in partition over two filters)

rpominov commented 8 years ago

@pixelpicosean Yeah, as @iofjuupasli said you are creating an infinite loop. Here is the logic of your code as it reads to me:

There is an infinite loop by definition, and introducing something like partition won't help in this case. If you can describe the logic you want (that doesn't include an infinite loop) maybe we find a way to express it in Kefir.

But it seems to me that we might be discussing 3 unrelated topics here :sweat_smile:

boneskull commented 8 years ago

@pixelpicosean I'm not understanding your code. Where does emitter come from?

Yet not sure this is what @boneskull wants, because this is exactly the same code as in original comment, and it should work.

Yes, it works, but it seemed kind of like I shouldn't have to actually write the second lambda.

pixelpicosean commented 8 years ago

@rpominov Sorry for bringing another topic, and I just found my problem is a little different from what you are talking about :sweat_smile:

I usually use kefir for game development, which includes tongs of states. The combination of a state and !state is very common. I create an example.

ps. FRP is still hard to use for me, but I'm trying to experimenting its the usage in game development which I believe is a uncovered super power for gamedevs. I'd like to hear from you pros :smile:

rpominov commented 8 years ago

Yes, it works, but it seemed kind of like I shouldn't have to actually write the second lambda.

I see, you can use something like http://ramdajs.com/0.19.1/docs/#complement then as @iofjuupasli mentioned for removing duplication.

boneskull commented 8 years ago

@rpominov To make sure I understand, let me rephrase:


function myFilter(value) {
  return value.foo === 'bar';
}

// given "complement" function not()
const notMyFilter = not(myFilter);

stream.filter(myFilter).onValue(value => doSomething(value));
stream.filter(notMyFilter).onValue(value => doSomethingElse(value));

Yes?

rpominov commented 8 years ago

@boneskull Yep, seems right.

boneskull commented 8 years ago

But yes, then it seems stream.not(otherStream) would be handy. If the mood hits you to implement such a helper, please consider this one. :smile:

rpominov commented 8 years ago

I don't think stream.not(otherStream) is possible, how do you know for every value from stream if it was or will be present in otherStream? We need to look in the future for this.

rpominov commented 8 years ago

@pixelpicosean I looked at your example, the problem is that you try to do A filterBy B where A and B have the same original events source, so it doesn't work well.

There probably no general solutions for this problem I'm afraid, each case needs to be considered separately.

boneskull commented 8 years ago

@rpominov I think that makes sense to me.

rpominov commented 8 years ago

@pixelpicosean I tried to fix your example http://jsfiddle.net/nd70ebdc/1/ , hopefully I got the intended behavior right :)

pixelpicosean commented 8 years ago

@rpominov That looks interesting, but seems not easy to scale. You're right that each case has its own solution. Are there any tricks to implement FSMs, or it is just not necessary?

rpominov commented 8 years ago

@pixelpicosean

but seems not easy to scale.

Have to agree with that.

Are there any tricks to implement FSMs

I think it can be done with scan: .scan((state, input) => ...)