Skip to content

The Scan confusion #29

@dmitriz

Description

@dmitriz

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):

  • The scan function in flyd is impure, strictly speaking. The simplest possible example is
const s = flyd.stream()
const s1 = flyd.scan(`add`, 1, s)
s1() //=> 1
s(1)
const s2 = flyd.scan(`add`, 1, s)
s2() //=> 2

So 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 a sinkStream is used to create a "ProducerStream", then scan is followed the .at() evaluation, and then by subscribe, 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 the scan 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 like scan are not allowed. That way scan 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 to scan 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 either Behavior<Stream<A>> or Behavior<Behavior<A>>.

The implementation (as always) varies among libraries:

  • flyd (and mithril/stream) allows scan on any stream and returns stream

  • MostJS scan regards all Streams as "cold", with the additional mosj-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 the fold 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 into
    what 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 the seed 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 other scans.

  • 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 of scan, while preserving the purity.

  • The scan function for the Pending 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions