funkia / hareactive Goto Github PK
View Code? Open in Web Editor NEWPurely functional reactive programming library
License: MIT License
Purely functional reactive programming library
License: MIT License
In the the documentation there is a "broken" link ... Even in Google caches it doesn't exists ... Maybe(Just Nothing)) ;)
We are using Hareactive in one of our projects and would like to cite it in a paper. Is there a preffered way by the core contributors to do so?
We should figure out how to run tests across browsers. The solution should be compatible with some CI service.
scanCombine([
[prepend, (item, list) => [item].concat(list)],
[removeKey, (keys, list) => list.filter(item => !contains(itemToKey(item), keys))],
], initial);
I conceptually understand how sample
solves the hot and cold observable problem, but I don't have a strong in-my-bones intuition for it yet. Part of my confusion is that I don't see any alternative for "the first moment in time" of when to start accumulating. Isn't it always "when the code runs"? When would we want a delayed accumulation time?
What might solve this confusion for me is to show how sample
allows us to model both hot and cold observables.
Consider a "behavior" such as the state of some data on a remote system (perhaps accessible via a REST API). The state exists at all points in time, but in order to access it locally I need to communicate asynchronously with the remote machine over the network.
In something like rxjs, I don't really have a choice: everything must be modeled as a stream. I'd probably do something like const req = new Subject(); const responses = req.flatMap(_ => updateState()).publish()
, subscribe my response handling to responses
, and do req.next()
whenever I want to update the remote state. Or perhaps something simpler, like Observable.interval(1000).flatMap(_ => updateState())
, although this is worse for granularity.
So in hareactive, do I model this kind of thing as a Behavior<Future<TState>>
or something?
I can't find any docs for moment
And, the difference from lift
isn't clear. Over here you explain it kinda,
funkia/turbine#51 (comment) but that should be more promiment
I can't find any docs for accumCombine
. I think I see what it does, though.
I'm sure there's more but that's just what I noticed while looking through TodoMVC.
Are these docs autogenerated? I could try to write some docs but I want to make sure I understand if they're gen'd.
Hi @paldepind. I opened this issue as a feedback to your mail. Sorry for the delay.
I'm going here to share some conclusions on my earlier experiments and my readings (I'd be happy to take part on this but unfortunately I cant find more time to work on another project actually. But I'll be glad to discuss any issue with you on this repo. I could also contribute later If I find the time)
I think it's important to define a denotational model before starting the implementation ( As Conal mentioned there are 2 fundamental requirements to define an FRP system: Precise denotation and Continuous time.). It's not required to be a formal model with denotational semantics. One can do it simply with a pure language (Haskell) or even with a pure subset of JS to TypeScript. For example take a look at reactive banana model.
IMO, it'd be better to take the semantic model described in this paper (the model is described in the first 4 pages) instead of the Classic model. It uses standard type classes (Functors, Applicatives, ...). It's not necessary to follow the implementation defined in the rest of the paper.
As for the implementation, In my experiments I was mainly focused on reactive behaviors. Traditionally (in non pure languages like JS, Java or Scala) they are implemented on top of Event Emitters & dependency graphs but as you mentioned coming up with a memory-leak free implementation is quite challenging without native weak maps. in my cfrp lib (which TBH is really far from the classic FRP) I distinguished 2 types of behaviors
Computed behaviors can be evaluated lazily, since they entirely depend on their sources.
Root behaviors, OTOH, are eager by nature, they must be 'updated' on each event occurrence. We can't evaluate them lazily unless we keep the entire event history since the last evaluation (which may cause serious space/time leaks, this was the issue with my pull based implementation unReact). The issue is to prevent memory leaks, especially with dynamic behaviors: A behavior will typically subscribe to its source events, but how it should unsubcribe from them?
There is another interesting alternative to traditional dependency graphs and it uses pure FP concepts to build a glitch free dependency model. The solution was presented by Conal Elliott in this paper. Basically it defined a Source
of value as a couple of Extractor (to extract a value) and a Notifier (to notify changes)
type Extractor a = IO a
type Notify = IO () → IO ()
type Source a = (Notify, Extractor a)
Then define computation on Source
values using Functors & Applicatives. I found this solution more elegant and also simpler than traditional dependency graphs
i made a rough JS port (abstracting over IOs). The solution was part of Conal's attempt to build a push/pull implementation of FRP. But was eschewed in favor of an IO-free implementation (sort of, it used blocking threads to implement events on top of Futures). AFAIK this was the last Conal's implementation.
Small comment regarding b496b69.
Previously Stream
had an array of listeners and the methods push
and a publish
. push
contained the logic of the stream and would be implemented differently for each stream type. push
had to call publish
with a value and publish
would then deliver this value to all of a the streams children.
This meant that in a chain of streams like stream.filter(pred).map(fn).scan(acc).subscribe(impFn)
if a value was pushed to stream
it would go throw this chain of function calls before reaching impFn
:
publish -> push (filter) -> publish (filter) -> push (map) -> publish (map) -> push (scan) -> publish (scan) -> push (subscribe)
Even though all of the function call was in-lined it reduced performance. After b496b69 the above chain is shortened to:
push -> push (filter) -> push (map) -> push (scan) -> push (subscribe)
I.e. publish
has been eliminated.
It works pretty much like this: each stream assumes that it has only one child as a child
property. That is push
can just call this.child.push
to deliver its result directly to its child. This works fine as long as a stream actually only has a single child. When a stream has more than one child a special MultiConsumer
is installed instead of an actual child. MultiConsumer
does what publish
did before, i.e. delivering a value to a list of children. In this way we can get rid of publish
when it is irrelevant and seamlessly get something equivalent back whenever a stream actually has several listeners.
The performance improvements are decent:
------------------------- filter-map-scan -------------------------
Stream old 71.27 op/s ± 0.98% (81 samples)
Stream 106.01 op/s ± 2.37% (80 samples)
most 108.82 op/s ± 1.30% (82 samples)
---------------------------- Best: most ----------------------------
------------------------ map-map-map-stream ------------------------
Stream old 228.08 op/s ± 0.77% (86 samples)
Stream 256.93 op/s ± 0.32% (83 samples)
most 274.56 op/s ± 0.57% (83 samples)
---------------------------- Best: most ----------------------------
--------------------------- merge-stream ---------------------------
Stream old 30.43 op/s ± 0.42% (71 samples)
Stream 38.95 op/s ± 0.30% (87 samples)
--------------------------- Best: Stream ---------------------------
--------------------------- scan-stream ---------------------------
Stream old 299.44 op/s ± 0.72% (85 samples)
Stream 328.17 op/s ± 0.78% (87 samples)
most 455.17 op/s ± 1.31% (86 samples)
---------------------------- Best: most ----------------------------
Testing FRP code can sometimes be tricky and cumbersome.
To solve the problem we are working on a way to FRP code in a way that is declarative and convenient.
Here is a simple example. Let's say one want's to test this function:
function foobar(stream1, stream2) {
const a = stream1.filter(isEven).map((n) => n * n);
const b = stream1.filter((n) => !isEven(n)).map(Math.sqrt);
return combine(a, b);
}
Currently, such a function can be tested like this:
const a = testStreamFromObject({ 0: 1, 2: 4, 4: 6 });
const b = testStreamFromObject({ 1: 9, 3: 8 });
const result = foobar(a, b);
const expected = testStreamFromObject({ 1: 3, 2: 16, 4: 36 });
assert.deepEqual(result.semantic(), expected.semantic());
The testing feature is currently quite incomplete. And we completely lack a way to test code that uses Now
. I think testing Now
could be achieved by combining elements from how testing IO works and how testing behavior/streams work.
This issue is for discussion ideas and problems related to testing.
Given the scenario that occurrences are pushed from the outside world, such as incoming websocket messages, and needing to reply only when each of N streams have emitted since last reply, using the values of the streams to form the reply, how can this be accomplished with hareactive?
In some libraries, this is as simple as zip([ streamA, streamB ])
, but it isn't clear to me how to do it with the behavior mentality. Making behaviors of A and B and lifting them to form the reply is close, but it disregards the timing/state problem, which remains by far the hardest part of the whole scenario.
Here's a psuedo code example:
const messageA$ = Stream()
const messageB$ = Stream()
wsA.on('message', messageA$.emit)
wsB.on('message', messageB$.emit)
const reply$ = zip([ messageA$, messageB$ ]).map(makeTheReply)
reply$.on(reply => [ wsA, wsB ].forEach(ws => ws.emit('reply', reply)))
Firstly, I think the 0.0.9 javascript node_modules/future.js
file in the npm release is not in sync with the src/future.ts
. I can't find sinkFuture
to import and it is not in the JavaScript file.
I was trying to figure out if it was possible to replace Task
from folktale's data.task with the hareactive Future
:).
these parts:
function read(path) {
return new Task(function(reject, resolve) {
fs.readFile(path, function(error, data) {
if (error) reject(error)
else resolve(data)
})
})
}
...
concatenated.fork(
function(error) { throw error }
, function(data) { console.log(data) }
)
Just wanted to mention, the Aurelia framework talks about its own behavior concept which might get confused with one used here:
aurelia/templating-resources#137
https://www.tutorialspoint.com/aurelia/aurelia_binding_behavior.htm
You can think of binding behavior as a filter that can change binding data and display it in different format.
Just wanted to let you know.
It is probably too general term for something specific, so more future confusion is likely guaranteed :) There can be just too many things that people will like to call that way.
Perhaps something like "continuous eventless stream" would better describe the concept, even if too long. Or perhaps some abbreviation like CES? :) At least no one will try to use if for other things.
Might be interesting to take into consideration:
A Behaviour is an EventStream + Some value in memory.
http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming/1033066#1033066
Actually quite useful to describe e.g. the user's inputValue
as you want both the value and to react to its changes.
When a node (A) occurs multiple times as a dependency of another node (B), B will fire many times for each firing of A, and some of the firings will have wrong value.
The following test case illustrates the problem:
it("handles diamond dependencies without glitches", () => {
const source = sinkBehavior(0);
const b = lift((a, b) => a + b, source, source);
const spy = subscribeSpy(b);
assert.deepEqual(at(b), 0);
source.push(1);
assert.deepEqual(at(b), 2);
assert.deepEqual(spy.args, [[0], [2]]);
});
It fails with:
1) behavior
applicative
handles diamond dependencies without glitches:
AssertionError: expected [ [ 0 ], [ 1 ], [ 2 ] ] to deeply equal [ [ 0 ], [ 2 ] ]
The presence of value 1
indicates that the framework let the application observe inconsistent values for source
(0
and 1
in this case).
Is this the expected behavior? If not, are there plans to fix it?
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.
scan
function in flyd
is impure, strictly speaking. The simplest possible example isconst 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 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>>
.
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
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 scan
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 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.
I am not sure if this is from another language, a real term, or it is meant to be subscription?
https://github.com/Funkia/hareactive/search?utf8=%E2%9C%93&q=subscribtion
We need a loopNow
function to establish circular dependencies in a now computation. Similar to what loop
in Turbine does for Component
.
In tests line numbers are currently referring to the compiled JavaScript.
That is quite annoying 😄
As noted in #21 changes
doesn't work with pull behaviors. Instead it causes a runtime error. This should be documented.
Additionally, I see no documentation on which behaviors are push and which are pull.
And the distinction isn't mentioned in the readme at all.
I would be nice to have some tools to help with describing transitions with behaviors. I imagine an API like this:
const config = {
duration: 5,
timingFunction: linear,
delay: 0
}
const t = transitionBehavior(config, numberStream, initialValue);
numberStream
is telling which values the resulting behavior should make transitions to.
This together with a collection of timing functions would be a good start.
As a newcomer to FRP topics, I tried to spin up a toy project gluing this library to the UI, but have struggled to get off the ground (say, with a counter) because there aren't quickstart examples in this project I can find
I noticed while looking at TodoMVC that the following isn't fully typesafe,
return accumCombine(
[
[prependItemS, (item, list) => [item].concat(list)],
[
removeKeyListS,
(keys, list) => list.filter((item) => !includes(itemToKey(item), keys))
]
],
initial
);
where prependItemS: Stream<A>
and removeKeyListS: Stream<B[]>
, but item: any
and keys: any
.
I looked at the types,
export declare function accumFrom<A, B>(f: (a: A, b: B) => B, initial: B, source: Stream<A>): Behavior<Behavior<B>>;
export declare function accum<A, B>(f: (a: A, b: B) => B, initial: B, source: Stream<A>): Now<Behavior<B>>;
export declare type AccumPair<A> = [Stream<any>, (a: any, b: A) => A];
export declare function accumCombineFrom<B>(pairs: AccumPair<B>[], initial: B): Behavior<Behavior<B>>;
export declare function accumCombine<B>(pairs: AccumPair<B>[], initial: B): Now<Behavior<B>>;
I see that if you do this
export declare type AccumPair<A, C> = [Stream<C>, (a: C, b: A) => A];
It won't work because C will get bound once to the first element of the first element of pairs
.
Is rank-n polymorphism what is needed here? I've read about it a bit. Does TS support it?
The documentation is currently pretty bad.
Add
Stream
Behavior
Now
Currently in examples/simple/index.ts
the view function looks like this:
view({validB, lengthB}) {
return Do(function*(): Iterator<Component<any>> {
yield span("Please enter an email address:");
const {inputValue: emailB} = yield input();
yield br();
yield text("The length of the email is ");
yield text(lengthB);
yield br();
yield text("The address is ");
yield text(validB.map(t => t ? "valid" : "invalid"));
return Component.of({behaviors: [emailB], events: []});
});
}
This uses the general do-notation form Jabz. Maybe it is worth it to create a modified overloaded do-notation for creating views. It could allow for
string
and string
valued behaviors directly instead of using the text
function.input
in the example.Component.of
.view({validB, lengthB}) {
return componentDo(function*(): Iterator<Component<any>> {
yield span("Please enter an email address:");
const {inputValue: emailB} = yield input();
yield [
br(),
"The length of the email is ",
text(lengthB),
br(),
"The address is ",
validB.map(t => t ? "valid" : "invalid"),
];
return {behaviors: [emailB], events: []};
});
}
The downside is of course that complexity is increased. Additionally it might be harder to understand due to the extra magic that is going on.
https://github.com/funkia/hareactive#changesab-behaviora-streama
I am a bit confused about this one, e.g. if the behaviour is modelled by the mouse move, how is it translated into the continuous stream?
It is used in the example
https://github.com/funkia/turbine/blob/master/examples/zip-codes/index.ts#L41
in a way that feels a bit like throwing away the available information coming from the typing
events from the inputValue
, which might be easier to model as (discrete) stream for precisely this reason. As stream it would emit the new value at the right time and you could save a line and a method, it would seem.
I might be missing something of course.
I'm a newbie to FP (with a background 10+ years in OOP). Researching the various library/framework available I have approaded to this repository which I find really interesting and divulgative. This library and Turbine looks like a more interesting approach than cycle js for example.
The question is about library implementation.
I see you have user class and hereditariaty to implement your data structures with abbondand use of the 'this' keyword. So I suppose the library is developed in OOP, or I mistaking something?
If yes is not a controsense to implement a FP library using OOP?
Thanks in advance for the explanation and compliments for the great work.
Hard to implement but would be nice.
Use case:
const isAfterEnd = timeB.map((t) => t >= duration);
const afterEnd = filter((b) => b, changes(isAfterEnd));
However, I think that implementing this would require Stream
to also have a push
/pull
mode just like behaviors. I.e. the afterEnd
stream above would be in pull
-mode.
It seems that many methods can be derived from the following general accumulator combinator:
accum<A>(init: Behavior<A>, fnStream: Stream<Behavior<(a: A) => A>>): Behavior<Behavior<A>>
Here accum(init, fnStream)
begins with init
and is mapped successively through each function in the fnStream
:
const accum = (init, fnStream ) => t1 => t2 =>
fnStream
.filter({ time } => (time >= t1) && (time <= t2))
.map({ value } => value(t))
.reduce((acc, feed)=>feed(acc), init(t))
Now we can derive:
const stepper = (init, stream) =>
accum(Behavior.of(init), stream.map(a => Behavior.of(b => a)))
const switcher = (init, streamBeh) =>
accum(init, streamBeh.map(beh => t => a => beh(t)))
const scan = (fn, init, stream) =>
accum(Behavior.of(init), stream.map(a => Behavior.of(b => fn(a, b)))
Is this correct?
I've been thinking about how best to initialize behaviors and events.
Goals:
This is what we currently have. Initialization can be found by searching for "name.def".
{click: btnClick.def} = button("Click me")
This one looks very non-magical. One would have to find initialization locations by searching for ": name".
button("Click me", {on: {click: btnClick})
This one also has no magic. The def
function (or whatever it should be called) extracts events from the component.
def({click: btnClick}, button("Click me")
In this one the events
list specifies which events to create. But it doesn't work, because the result of the expression whould not be the component but instead the array.
[btnClick.def] = button("Click me", {events: ["click"]})
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.