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

combine() exposes inner state #105

Closed
svachalek opened this issue Aug 15, 2016 · 7 comments
Closed

combine() exposes inner state #105

svachalek opened this issue Aug 15, 2016 · 7 comments

Comments

@svachalek
Copy link

svachalek commented Aug 15, 2016

I noticed this when adding dropRepeats to the output of a combine; the stream stopped producing anything.

What appears to be happening is that the combine operator is producing an array, mutating it, and sending the same array again, so dropRepeats ends up comparing the array to itself, in its current state, and it always comes up equal.

It can be worked around by putting map(a => a.slice(0)) in between, but it seems to me this [passing the inner state to next()] is a dangerous practice since user code could end up naively mutating the array and causing subtle bugs.

@staltz
Copy link
Owner

staltz commented Aug 15, 2016

Yes I'm aware of this behavior, but before doing these changes to combine, can I know why are you using dropRepeats after combine? (It usually means you are looking for something like #102). The common use case for combine is to use a map just after it. We get performance benefits by not slicing the array. We could slice it internally in combine just to make combine more correct, but only if it supports recommended use cases.

@svachalek
Copy link
Author

The common case for me as well is to just map it into an object, but in this case it was essentially:

xs.combine(a$, b$, c$, $d).map(([a, b, c, d]) => /* HTTP call */).flatten()

This shouldn't come up in CycleJS but this is in a hybrid context so things are not quite as pure. I could dropRepeats on each input but it was less messy code-wise to stick a dropRepeats before the map.

It's definitely a corner case, maybe more worthy of an FYI in the doc rather than a code change. But I thought I'd bring it up just on the chance someone would walk away with the array reference and wind up with some really confusing behavior. There's probably some way to create an immutable proxy for an array that would prevent that too, although it would still trick the dropRepeats filter.

@Hypnosphi
Copy link
Contributor

Is it possible to dropRepeats on resulting request object in your case?

@staltz
Copy link
Owner

staltz commented Aug 16, 2016

I'm thinking that we could attempt to make this fix in dropRepeats instead of in combine.

@Hypnosphi
Copy link
Contributor

Hypnosphi commented Aug 17, 2016

How exactly? By performing deep comparison each time?

@staltz
Copy link
Owner

staltz commented Aug 17, 2016

Not by doing deep comparison inside dropRepeats, but deep comparison in the isEqual argument of dropRepeats. While the fix inside dropRepeats would be check if every incoming value is an array, and if true, then slice it before keeping in memory. So I'm suggesting the fix to be both in user code and library code. We have to also fix it on user code because of the following:

dropRepeats uses triple equality by default, which may mean equality by value (strings, numbers) or equality by reference (objects, arrays). You should avoid comparing by reference unless you actually want it. 'foo' === 'foo' is always true because it's equality by value (or contents if you will), while {} === {} is always false because it's equality by reference and these are references to two different newly created objects. So xs.of({}, {}, {}, {}).compose(dropRepeats()) will emit {}, {}, {}, {}, and that's not a bug in xstream, because dropRepeats() is dropRepeats(tripleEquals).

So because the combine stream is a stream of arrays, we shouldn't use tripleEquals, because even if we add a slice inside combine, if the outcome is a stream of [1,2,3], [1,2,3], [1,2,3] these are 3 different references to arrays, so dropRepeats would emit [1,2,3], [1,2,3], [1,2,3].

That's why it's better to do this fix using dropRepeats argument, with deep comparison or hashing, assuming that the internal memory of previous emissions in dropRepeats is not mutated:

streamOfArrays.compose(dropRepeats(deepEqual))
streamOfArrays.compose(dropRepeats((x, y) => hashOf(x) === hashOf(y)))

staltz added a commit that referenced this issue Aug 17, 2016
dropRepeats now checks if the incoming stream value is an array, and slices/clones it before keeping
it in memory. This is to support using dropRepeats after a combine and in other use cases where the
input stream emits arrays.

Closes issue #105.
@staltz
Copy link
Owner

staltz commented Aug 17, 2016

Fixed in v5.3.6

@staltz staltz closed this as completed Aug 17, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants