-
Notifications
You must be signed in to change notification settings - Fork 29
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
The Scan confusion #29
Comments
I'm not really a fan of |
That sounds like a long departure from I can see that you may consider it a less cleaner abstraction, But I am sure you have thought it well through, so I might be wrong. |
Flyd is still there for people who want to use that 😄 A key feature in Hareactive is that both stream and behavior have a precise and simple semantics. All operations can be understood in terms of the semantics. I can't think of semantics for MemoryStream that doesn't basically make it a behavior. |
I had some more thoughts and found some conceptual difficulties when seeing streams as lists of time-value pairs. When you consider a pure function Those features seem to be lacking for the streams when seen as complete lists of time-value pairs. You can't pass it as value because you don't know the future part of the list. Also you don't want your pure function to depend on the past events, which means the value of On the other hand, these problems seem to disappear if you regard streams as channels that can produce values at moments in the future. You can pass such channel as value to a function. And the function can be seen as set of instructions what to do when a value appears in the channel. Then the purity can be stated in terms of those instructions applied to the same values. That is strictly stronger property than simply being determined by the complete list. For instance, two lists producing the same values at different moments have to be considered different, but the instructions can be the same, and you can still test for the same results. Also you can't pass as value the entire behavior as function of time. You can only pass the channel that will keep updating its value from that moment. And you look for the stronger purity property that the resulting behavior at any moment is determined by the input values only up to that moment. And the If that works, it might be a simpler model where the |
I'm not sure I understand the problem nor the solution. A stream is only a list in the semantic sense. Considering a stream a list gives us a mental model for understanding streams and the operators on it.
That is pretty much how a stream is implemented. But it not a precise semantic. |
The problem is to understand the signature: Here the last parameter is the value of type stream: You can only pass the channel, not the list, so maybe the channel should be the argument type? Another problem is the return value, for which you are getting scanS<A, B>(fn: (a: A, b: B) => B, startingValue: B, stream: Stream<A>): Stream<B> Here both streams are just the channels without any list semantics. Is there any problem with this approach? |
This sounds very much like a Stream in hareactive.
Assuming that by complete list, you mean a finite list, The semantics of Stream does not require the list to be finite.
If I understand your channel correct, a definition would be a pair of a timestamp and a list of occurrences occurring after the timestamp.
With this definition, you should be able to make a pure |
Because, what is a channel? A stream is clearly defined. It's a list of pairs with time and value. Such a thing can be understood precisely and mathematically. And from this semantics, we can explain all the other operators. For instance, the semantics of const map = (fn, stream) => stream.map(([time, value]) => ([time, fn(value)]); What is the semantics of a channel? What is the semantics of
I don't understand how that signature can be pure.
There is no problem testing
The semantic makes sense. But, I don't think you could make |
I mean the channel mostly in the sense it is used in Go https://gobyexample.com/channels or Javascript https://github.com/ubolonton/js-csp, inspired by Go, without any advanced functionality. Precisely a stream channel is a variable that can receive values at the future moments. The It is similar to the list of pairs, except it is more realistic because it is closer to how you use the streams. The list model is simpler but not as realistic, since you never have that list in practice, only parts of it.
In the sense that the values of the return channel are given as pure functions
I am not entirely sure how the methods there are working precisely.
With the channel model you don't need to return the time, only value at the right time. |
I think we are talking past each other. What you're describing as channels doesn't sound any different from what we call streams. You're just explaining it in natural language.
That is not precise in the way I mean. Our semantics for streams is precise because it explains streams by mapping them to a corresponding mathematical object. The semantics for streams uses lists, pairs and time (real numbers). All of these are mathematical objects. If what I mean doesn't make sense then you may want to take a look at this great article Semantic Design, the section in my blog post titled "Thinking like Conal" or maybe this talk by Conal Elliott. |
I had to think how to define it mathematically. I surely love the simplicity of the classical FRP approach, Another issue was, the idea of the channel (or signal) was not really mine, So if I am to try to define it mathematically, Now let me try to define the However, I have to admit, that model would suffer from extra complexity that I don't particularly like myself. Which also makes me like more the classical FRP model, even though it presents some bigger mental shift away from the traditional RP (without the "F"). Let me try to explain what I mean here. The Conal's FRP model requires us to think at once about the whole lifetime of our time-changing value as function of time. That is very natural when you compare with how the moving objects are described in Physics but different from the RP way of handling the streams and observables. Here I'd find the analogy with Physics the easiest way to explain as most people learned it in school. But with streams, we are getting some additional confusion. As @limemloh mentioned, the Stream is not required to be a finite list. However, I presume the list is still required to be locally finite, that is having finite intersection any finite time interval. On the other hand, finite lists may suffice to define all the same methods and would make our model simpler. In any case, it would be good to include the finiteness aspect in the definition to avoid this confusion. Another confusion is the difference in As a matter of disclaimer, please don't take it as any critics in any way.
The simplest way I can see it, you have two basic operations here -- truncation and reduce. You need two time moments For each of the moments Hope you find it useful. I will write more feedback on the other parts as this post is already getting long :) |
I think your model for Channel is interesting. I actually don't think the thing is very far from what Stream is in Hareactive.
Yes. Exactly. We need to forget the past in
At the semantic level, the whole lifetime is included. For instance, something like I think the analogy to physics is very good 😄. The way moving objects are described in physics is identical to what a behavior is in FRP. We can even implement something similar to what people learned in school in Hareactive: const physics = go(function* () {
const acceleration = Behavior.of(1); // constant behavior
const velocity = yield sample(integrate(acceleration));
const position = yield sample(integrate(velocity));
});
I've never thought about that. But yes, that is true. |
The
scan
function seems to be both common and useful,but it also caused me some confusions like here and here that I'd like to clear if possible.
Here is my current understanding (appreciate any correction):
scan
function inflyd
is impure, strictly speaking. The simplest possible example isSo the result depends on the time of calling the
scan
.I would like to run the same example with
Hareactive
, but at the moment it is not quite clear to me what would be the best way. From the scan tests I see that asinkStream
is used to create a "ProducerStream", thenscan
is followed the.at()
evaluation, and then bysubscribe
, whose role is not quite clear to me, in particular, whether it turns stream from pending into active, like other libraries do. Then events need to be published. I wonder if there were any more direct way similar to the above.It can be seen as composition of truncating the stream events between two moments into array (forgetting the times) and subsequent
reduce
. The latter part is pure. The impurity comes from the implicit dependence on the first moment (the moment when thescan
was called). It is still pure in the second moment, which is the current time.The
scan
becomes pure when applied to the so-called "cold" (I find "pending" more descriptive) streams, the ones that had not received any value yet. This is how they do it in MostJS. Any of their method "activating" the stream, transforms it into Promise, after which methods likescan
are not allowed. That wayscan
remains pure.Applying
scan
only to the pending streams is the most common use case, as e.g. @JAForbes was suggesting in Allow optional seed in Stream.scan? MithrilJS/mithril.js#1822 . Such as the action stream is passed toscan
before at the initialisation time, whereas the actions begin to arrive after. This fact is also confirmed by the absence of tests in the non-pending cases, for instance, note how in https://github.com/paldepind/flyd/blob/master/test/index.js#L426 the stream is always created empty.The 2
scan
methods here are pure, however, they differ from other libraries in which they return the more complex types of eitherBehavior<Stream<A>>
orBehavior<Behavior<A>>
.The implementation (as always) varies among libraries:
flyd
(andmithril/stream
) allowsscan
on any stream and returns streamMostJS
scan
regards all Streams as "cold", with the additionalmosj-subject
to use with active streams, however, the purity is lost in that case.Xstream does not have
scan
, it seems to be replaced with thefold
which "Combines events from the past throughout the entire execution of the input stream". That seems to solve the purity problem for them, but may not be as convenient to use.KefirJS https://rpominov.github.io/kefir/#scan and BaconJS https://baconjs.github.io/api.html let their
scan
to transform stream intowhat they call "property", which I understand is the same as Behavior here.
I am not familiar with details but they seem to talk about "active", so possibly they way is similar to MostJS.
The
RxJS
makes theseed
argument optional. Which, however, presents problems if it is of different type than the accumulator. (They only demonstrate the simple addition case, where the types are equal.)The same is in KefirJS
Possible suggestions:
Change the name to something like
pureScan
to emphasise both the difference and the motivation for the additional complexity, and to avoid the confusion with the otherscan
s.I would like to have some safe and simple variant. Like stream and behavior in one thing, what Xstream calls the Memory Stream. So I know that both stream and behavior would conform to this
memoryStream
interface and I don't have to think which one is which. It may be derived from other more refined versions, but I would like to have it for the sake of convenience.A new
MemoryStream
interface might be a great entry point to build adapters for other libraries as it would accept all streams, promises, behaviours, properties and even observables. So people can use their old code with the api they understand, which is great.A new
Pending
interface could be combined with Streams, Behaviors, or MemoryStreams. It would allow the use the "unlifted" version ofscan
, while preserving the purity.The
scan
function for thePending
interface could be called something like "pendingScan" to emphasise its use case. It would only apply to pending memory streams, in its pure form. Its impure brother can be called "impureScan" and would apply to any memory stream, but with "impure" in it's name, it is no more the library's responsibility :)The reducer would gets called and the initialisation time (when the stream is not pending) and when the events are emitted.
Let me know what you think.
The text was updated successfully, but these errors were encountered: