Comments (16)
The destroy behavior is something that should definitely be changed! The current implementation has very limited usage. I agree with you that something should be done to a streams listeners when a stream is destroyed.
This is my thinking: Flyd streams can be divided into two types
- Streams that doesn't make sense without all of their dependencies. For instance streams created by
flyd-sampleon
andflyd-lift
. - Streams that works well just as long as some of their dependencies are active. For instance, streams created by
flyd.merge
.
This is sparsely documented, but the way you create streams of the different types depends on the third parameter to flyd.stream
(it's poorly name doesNotRequireDeps
in the README). That's why the implementation of flyd.merge
passes true
to stream
. Because the created stream can work without all of it dependencies.
This is the behavior I think would be optimal: Streams that require all of their dependencies are destroyed as soon as one of their dependencies are destroyed. And, streams that just need some of their dependencies are destroyed only when all of their dependencies have been destroyed.
Does that makes sense to you? What do you think?
from flyd.
I also think destroy
should be renamed to end
.
from flyd.
doesNotRequireDeps = undefined
can be implemented with doesNotRequireDeps = true
var deps = [a, b];
// all deps should has value
flyd.stream(deps, function () {
if (deps.some(function (dep) {
return dep() === undefined;
})) {
return;
}
return a() + b();
}, true)
So doesNotRequireDeps = true
is more low-level.
I think there can be two methods. And one that implements same behavior as with doesNotRequireDeps = undefined
will use more general method.
flyd.stream(fn, deps)
should be most low-level method. So it calculates new value every time, even initially without values on dependencies.
flyd.combine(fn, deps)
is implemented over stream
and uses code like in example above. Its behavior will be same as current flyd.stream(deps, fn)
About destroy:
var a = flyd.stream();
var b = flyd.stream();
var c = flyd.stream([a, b], function () {});
c.onEnd = function (streamEnded) {
if (a === streamEnded) {
return;
}
c.end();
}
a.end();
b(1); // `c` recalculates
a(1); // throws exception
a(); // returns last value
b.end(); // c ends
For most general low-level method stream
it should give user all possibilities to customize ending behavior.
onEnd
called when one of dependencies ends.
Streams implemented over flyd.stream
can add onEnd
method: map
should end when its single dependency ends, combine
when one of dependency ends, and etc.
By default, I think, flyd.stream
should end only when user calls .end()
. It's possible for stream to emit values even when there is no one active stream in dependencies. e.g. interval
.
from flyd.
These are interesting ideas! Thanks a lot for sharing them.
Yes, you're right that doesNotRequireDeps = undefined
could be considered more low level. The implementation you've posted is pretty close to the one in Flyd.
What do you think the advantage is of having stream
and merge
versus just having stream
with a parameter that switches behaviour? I like the elegance of having a single low level function on top of which everything can be implemented.
Another approach to ending streams: when a stream ends it triggers an update on listening streams if they have doesNotRequireDeps = true
but with a flag that indicates that the stream has ended:
var a = flyd.stream();
var b = flyd.stream();
var c = flyd.stream([a, b], function (self, changed) {
if (changed.ended) {
// `changed` stream ended
if (changed === b) flyd.end(self);
}
}, true);
flyd.end(a)); // c is called with ended `a`
flyd.end(b)); // c is called with ended `b` which ends `c` as well
This design makes an explicit onEnd
method unnecessary.
By default, I think,
flyd.stream
should end only when user calls.end()
. It's possible for stream to emit values even when there is no one active stream in dependencies. e.g.interval
.
Hm, interval
wouldn't have any dependencies as all would it? So there would be no dependencies to end it.
from flyd.
What do you think the advantage is of having stream and merge versus just having stream with a parameter that switches behaviour?
return flyd.nameIt1([a, b], function () {
// do something with both a and b
})
return flyd.nameIt2([a, b], function () {
// do something with a or b (or neither)
}, true)
For first function best name, I think, combine
. Same name used in almost all Rx libs. There is a difference in arguments passed to function, but combine
is closest name.
Second can be named merge
, but it'll be actually wrong. merge
should just emit from all dependencies. With nameIt2
it's possible to do practically all known methods on stream.
Why it should be two separate methods? Because it has different intent. And nameIt1
is just shortcut for nameIt2
with checking values at beginning of body
.
I think, the most flexible method should be used as basic building block.
It introduce more verbose body
functions. But it's ok, I think, because reducing code duplicating is in high-level methods responsibility.
Another approach to ending streams: when a stream ends it triggers an update on listening streams if they have doesNotRequireDeps = true but with a flag that indicates that the stream has ended:
It'll be same as trigger an update with {reduced: true}
and check this in 'body'. It requires every body
in listeners to check this. So calling .end()
will be a pain if checking was omitted in some stream.
It's better to warn user when .onEnd
isn't provided, and that it should produce memory leak.
For doesNotRequireDeps = true
if it means flyd.combine
.onEnd
can be provided in core:
function combine(body, deps) {
var stream = flyd.stream(function () {
if (deps.some(function (dep) {
return !dep.hasVal;
})) {
return;
}
return body.apply(null, arguments);
}, deps);
stream.reduce = flyd.stream(function (self, changed) {
if (!changed) {
return;
}
// any value means reducing
return true;
}, deps.map(function (dep) {
return dep.reduce
}));
}
Hm, interval wouldn't have any dependencies as all would it? So there would be no dependencies to end it.
My js is probably better than english )
βTalk is cheap. Show me the code.β (c) Linus
So here is what I mean. Or why it's hard to use any default function for reduce
.
var a = flyd.stream();
var intervalled = flyd.stream([a], function (self) {
if (!a.hasValue) {
// a is required for this stream
return;
}
var value = a();
setInterval(function () {
self(value);
}, 1000);
});
var c = flyd.stream([intervalled], function () {
if (!intervalled.hasValue) {
return;
}
return intervalled() + 1;
})
a(1);
a.reject();
If we reject intervalled
because of all its deps rejected, it means that we should reject
c
-stream too. But intervalled
stream looks ok, it has no deps, but produce values.
So how should looks default strategy for all streams? Or for this case it should be overrided?
from flyd.
For first function best name, I think, combine. Same name used in almost all Rx libs. There is a difference in arguments passed to function, but combine is closest name.
Second can be named merge, but it'll be actually wrong. merge should just emit from all dependencies. With nameIt2 it's possible to do practically all known methods on stream.Why it should be two separate methods? Because it has different intent. And nameIt1 is just shortcut for nameIt2 with checking values at beginning of body.
combine
sounds too much like merge
. And as you noted merge
is a different thing. I think that this naming would be confusing. And I think the reason why your names are so identical is because the two functions you're proposing does pretty much the same thing. Are more descriptive naming would be stream
and streamManualDeps
. But then why not just use a boolean parameter like now?
The way I see it they both have the same intent: the intent to create streams that depends on other streams. There's just a difference in how they handle dependencies and that difference, I think, can be expressed just fine with an optional parameter.
It'll be same as trigger an update with {reduced: true} and check this in 'body'. It requires every body in listeners to check this. So calling .end() will be a pain if checking was omitted in some stream.
Only functions that has doesNotRequireDeps = true
will have to deal with ended streams. Other streams will just close when their dependents closes (and let me note that almost all stream are of the second type, i.e. they need all their dependencies).
I do not understand the combine
function you've defined. What does the reduce
function you attach to the stream do?
Your interval function is a bit odd. You read the value from the stream a
only one single time! As I said before I don't think an interval function would have any dependencies. Take a look at the interval functions in Kefir og Bacon. They just take a value β not a stream.
With my proposal a delayed stream could be created like this:
var a = flyd.stream();
var delayed = flyd.stream([a], function (self, changed) {
if (!a.hasValue) {
// a is required for this stream
return;
}
if (changed.ended) {
setTimeout(function () {
flyd.end(self);
}, 1000);
}
var value = a();
setTimeout(function () {
self(value);
}, 1000);
}, true); // Pass true to manage dependencies the low level way
var c = flyd.stream([delayed], function () {
// This stream automatically ends 1 second after `a` ends
return delayed() + 1;
})
a(1);
flyd.end(a);
To me the approach seems powerful enough. And you force streams that decides to manually manage their dependencies to handle ended streams. Actually, reading from an ended stream could throw an error. Then it is exposed if a dependent stream doesn't correctly handle ended streams.
from flyd.
c86b913 makes the following changes:
- Adds an
onEnd
methods to streams exactly like you suggested (I actually misunderstood this at first, I thought you meant a function like BaconsonEnd
). I am however considering renaming this to something likeonDepEnd
. Streams are given a defaultonEnd
implementation that ends the stream as soon as the first dependency ends. So supplying the method is completely optional and no additional burden is placed on streams in the common case. - Removes
flyd.destroy
. - Adds
flyd.end
that callsonEnd
on it all its dependents.
I am very happy with how little code had to be added and I think this approach solves the problems in a very elegant way.
I have not pushed this to npm yet though. I'd very much like to hear what you think about this @iofjuupasli.
from flyd.
@paldepind awesome work.
Now ending possible without headache.
However my last example with reduce
is more flexible and powerful, I think. Probably I misnamed it.
It's stream of end events. So it possible to end stream when some another stream have some value, or some stream have emitted end.
Stream that ends after 1s after creation:
s.ending = flyd.stream([], function(self){
setTimeout(function(){self(true)}, 1000);
}, true)
Stream s
ends right as a
ends:
s.ending = a.ending;
Stream s
ends when a
emits 0
:
s.ending = flyd.stream([a], function(){
if (a() === 0) { return true; }
})
s
ends when a
ends or killerStream
emits any value;
s.ending = flyd.merge([a.ending, killerStream])
And so on. For me it looks really flexible and simple to understand.
After .ending
emits any value, it unsubscribe streams from current stream, and also destroy .ending
stream on itself.
And with .ending
by default with same as current behavior
from flyd.
I'm sorry but I don't understand your idea. What is the ending
stream you attach to streams? The stream in ending
ends the stream it is attached to when it emits a value?
Why not just to this for a stream that ends after 1s:
var s = flyd.stream([someDep], function(self) {
setTimeout(function() { flyd.end(self); }, 1000);
return someDep();
});
And this for a stream that ends when its dependency emits 0
:
var s = flyd.stream([a], function(self) {
if (a() === 0) {
flyd.end(self)
} else {
return a();
}
});
from flyd.
The stream in ending ends the stream it is attached to when it emits a value?
Yes.
But it also make possible to subscribe to ending of any stream. And for some cases shorter.
var s = flyd.stream([b], function(){
return a() + b();
})
s.ending = flyd.merge([a.ending, killerStream])
How you'll implement this without ending stream?
You can say that it's rare case. But it's so flexible, that there is no extra code for simple cases in your examples, but gives more possibilities. For me it looks like win-win.
Maybe you can find better name for ending
, not sure that it's descriptive enough.
from flyd.
Ok, I get it now. I do like the idea! It is indeed very flexible and quite neat.
But does it make sense for a streams ending to depend on something that is not one of its dependencies? I'm not sure but having the flexibility seems nice!
You also have the issue that due to atomic updates the body of the ending stream will only be called once if several streams ends at the same time. Like this:
var a = stream(1);
a.ending = flyd.stream([a], function(){
if (a() === 0) return true;
});
var b = flyd.stream([a], function(){
return a() * 2;
});
b.ending = flyd.stream([b], function(){
if (b() < 5) return true;
});
var s = flyd.stream([b], function(){
return a() + b();
});
s.ending = flyd.stream([a.ending, b.ending], function(self, changed) {
// I'm only called once
console.log(changed + ' ended');
});
The body of s.ending
might expect to be called once for every ended stream? See for instance how I've implemented flyd.merge
. It counts how many streams are left based on how many times it was called. How would you implement it with your proposal?
from flyd.
But does it make sense for a streams ending to depend on something that is not one of its dependencies?
I like flyd for its flexibility so that it doesn't force you to write only idiomatic code. In flyd.stream it's possible to read value from another stream not from dependencies. But sometime it gives capability to write much better code, more readable, but with same semantic as with bacon or similar. And sometimes it's really right way to write FRP code.
You also have the issue ...
Not sure what do you mean exactly.
s.ending = flyd.merge([a.ending, b.ending])
Or maybe same as current with stream:
var deps = 2;
s.ending = flyd.stream([a.ending, b.ending], function(){
if (--deps === 0) return true;
}, true)
from flyd.
I like flyd for its flexibility so that it doesn't force you to write only idiomatic code. In flyd.stream it's possible to read value from another stream not from dependencies. But sometime it gives capability to write much better code, more readable, but with same semantic as with bacon or similar. And sometimes it's really right way to write FRP code.
Yes. You are completely right! Being unrestricted, powerful and unlimiting was the design goal from the beginning. I've thought some more about this and I really like the power your proposal gives. And I can think of situations where that power would be useful!
The problem is that if a.ending
and b.ending
ends at the same time the body will only be called once due to atomic updates and thus the count will be incorrect.
I think a solution could be to make changed
an array of all streams that has changed in the given update. That could be useful in other situations as well.
from flyd.
I think a solution could be to make changed an array of all streams that has changed in the given update. That could be useful in other situations as well.
That would be nice. Agree that it could be useful in general.
from flyd.
49af1e3 adds this end functionality!
You can now do stuff like this (I've renamed ended
to end
):
var x = stream(3);
var xEq0 = stream([x], function() {
if (x() === 0) return true;
});
var y = stream([x], function() {
return x();
});
flyd.endsOn(xEq0, y);
flyd.map(function() {
console.log('y ended');
}, y.end);
x(2);
x(0); // `y` ended
By default a stream still ends as soon as one of it's dependencies end.
Implementing this was a bit tricky. Every stream has an end stream, but the every end stream would have and end-end stream. That recurses forever:
var s = flyd.stream();
s.end; // <- That is a stream
s.end.end; // <- Is that also a stream?
s.end.end.end; // <- Does this go on forever?
The current implementation breaks the chain by not giving the default end stream an end stream.
Anyway, I think the current implementation has room for some refactoring.
from flyd.
I've added initial documentation for how stream endings work to the readme.
Thanks a lot @iofjuupasli for sharing your great ideas!
from flyd.
Related Issues (20)
- add more dependency for existing combine HOT 7
- Merge stream delay HOT 6
- add package.module
- Drag 'n Drop example is broken HOT 2
- Compare previous state with current state HOT 14
- Feature Request - Supply metadata for a combined stream HOT 2
- Help to build control flow func HOT 3
- Do you think this Promise -> Stream conversion is a right alternative way? HOT 6
- Wrong chain + setImmediate behavior HOT 1
- Benchmark against Most.js HOT 3
- Can flyd streams be made lazy ? HOT 9
- HELP: Trouble reading api documentation HOT 1
- Enhancement: Add Readonly-Streams (at least in Typings) HOT 3
- What's the motivation for ending a stream? HOT 3
- question on serialize stream
- from Node stream HOT 2
- Question: Required (code) complexity for higher-order streams HOT 10
- Support multiple operators for Pipe HOT 4
- [Help Needed] CombineLatest in the style of RxJs HOT 17
- Switch Latest without Ramda Dependency HOT 6
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. πππ
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google β€οΈ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from flyd.