baconjs / bacon.js

Functional reactive programming library for TypeScript and JavaScript
https://baconjs.github.io
MIT License
6.47k stars 331 forks source link

Bacon.js 4.0 #771

Open raimohanska opened 3 years ago

raimohanska commented 3 years ago

Placeholder issue for all things that might be included in 4.0. Post your ideas and vote with emojies. Knock yourself out!

raimohanska commented 3 years ago

Property reactivation alternative is something to consider: #770

raimohanska commented 3 years ago

"Property shall always have a current value" is something that's super nice for GUI applications where you want to render a Property's value on the screen. Instead of a white screen you want a value that representing the status { status: "loading" }.

Not at all sure what would be the negative implications here.

raimohanska commented 3 years ago

Property.get() for extracting the current value of a Property is something I've found useful, also in applications rendering a Property's current state. Yes, I'm kinda reverting my earlier stance of "there shall be no currentValue() method" :) The method must though throw in case there is no current value or in case the property is not active (no subscribers). So this would be only for situations where you are sure you do have an activated property.

semmel commented 3 years ago

"Property shall always have a current value"

… is something that's super nice for GUI applications where you want to render a Property's value on the screen. Instead of a white screen you want a value that representing the status { status: "loading" }.

Indeed! For that purpose I have begun to put Maybes in some of my GUI properties. I initialise with .startWith(nothing()) or .toProperty(nothing()) so that my combined GUI state property always has a value which can be rendered. When the GUI state gets combined I decide per property how to interpret a nothing; as empty string, empty array of false boolean...

If you suggest by your comment, that v.4.0 enforces initial values for properties, I'd regard it beneficial.

Static Function API for 4.0

As already stated somewhere else I no longer like dot-chaining Baconjs pipelines, i.e. glueing Observable method calls, but I'd rather compose static functions which

This makes Baconjs play nice with functional libraries like Ramda. Btw @most/core has such an API.

Here is an example of that style.

A small step into that direction would be to make Observable conform to the FantasyLand (FL) Specification. (I guess the .map method already OK, but all the ugly ['fantasy-land/of'], ['fantasy-land/chain'] - which is .flatMap() methods would have to be implemented. This way a FL-aware FP library like Ramda already provides a good part of the static functions needed to compose a baconjs pipeline.

However, for the arguments made in this article by James Sinclair I prefer the StaticLand specification over FL. Thus Baconjs had also provide a static map, chain, filter etc. Currently I use a wrapper library for that purpose.

raimohanska commented 3 years ago

I've also thought about changing the API from method chaining to static method piping. That would be awesome! I'm all in for going into that direction, given that someone had the energy and time to actually do the hard work :) This might not prove a huge undertaking, given that the operators are already defined as static functions in individual files, while the Observable classes just define methods that delegate the work to the static functions.

Wanna give it a shot?

semmel commented 3 years ago

Wanna give it a shot?

Yeah could do it. However my limited knowledge of TypeScript stems from the time I briefly worked with ActionScript 2.0, so I would not want to ponder much on the typings.

raimohanska commented 3 years ago

...but this might be a great opportunity for you to hone your TypeScript skillz to the next level :) I didn't know much TS either before I migrated Bacon from ES to TS.

semmel commented 3 years ago

@raimohanska

...but this might be a great opportunity for you to hone your TypeScript skillz to the next level :)

I don't know. I did quite a lot in C++ where anything cool involves template classes and functions - but they're hard to get right and add so much noise. Moving to dynamically typed languages like JavaScript felt like getting rid of chains holding me back. If I had the choice now I'd migrate to something like ReasonML where you have the benefits of type safety but it is the compiler who figures it all out and the code still looks nice.

Anyway, let's make Baconjs nice!

(Perhaps I will also draw some inspiration from @most/core which is also written in TS)

steve-taylor commented 3 years ago

Consider the following code:

const stream$ = Bacon.once('Hello')

const unsubscribe1 = stream$.onValue(value => console.log('1:', value))
const unsubscribe2 = stream$.onValue(value => console.log('2:', value))

unsubscribe1()
unsubscribe2()

Output in v1:

1: Hello

Output in v2 (I think) & v3:

Proposed output in v4:

1: Hello
2: Hello

This would make EventStream synchronous again. Additionally, it would involve EventStreams temporarily remembering any events generated during initialization and emitting them to the 2nd and subsequent subscribers that subscribe in the same clock tick as the first subscriber. Upon storing these initial synchronous events, the EventStream would queue their removal via queueMicrotask.

steve-taylor commented 3 years ago

remember would require Observable / EventStream to have listeners that can detect subscribers and, on subscription, send the current value, if any, as an initial event.

steve-taylor commented 3 years ago

Controversial idea: Consider renaming the library for broader (non-carnivorous programmer) appeal. I realize the name refers to the philosopher rather than the meat, but many wouldn't take the time to find out and, even if they did, it still has the meaty connotation. It doesn't bother me personally, but it might be hurting adoption.

steve-taylor commented 3 years ago

Add a collect method to Observable that takes a collector and returns a Promise.

Assuming the generic type of Observable is VALUE:

type Collector<FROM, TO> = (source: Observable<FROM>) => Promise<TO>

class Observable<VALUE> {
    // ...

    collect<TO_VALUE>(collector: Collector<VALUE, TO_VALUE>): Promise<TO_VALUE> {
        return collector(this)
    }
}

Proposed collectors:

Examples:

v3 v4
stream$.toPromise()
stream$.collect(toLast())
stream$.firstToPromise()
stream$.collect(toFirst())
stream$
    .fold([], (acc, value) => [...acc, value])
    .toPromise()
stream$.collect(toArray())
stream$
    .fold([], (acc, value) => [...acc, value])
    .toPromise()
    .then(array => new Set(array))
stream$.collect(toSet())
stream$
    .fold([], (acc, value) => [...acc, [value.id, value]])
    .toPromise()
    .then(keyValuePairs => new Map(keyValuePairs))
stream$.collect(toMap(({id}) => id)
stream$
    .fold([], (acc, {id, ...value}) => [...acc, [id, value]])
    .toPromise()
    .then(keyValuePairs => new Map(keyValuePairs))
stream$.collect(toMap(
    ({id}) => id,
    ({id, ...value}) => value
)
raimohanska commented 3 years ago

I started experimenting with a new library that deals with my grievances with Bacon when used in UI programming. Go check out: https://github.com/raimohanska/lonna.

Totally experimental for now. I'm a bit excited about it anyway :)

semmel commented 1 year ago

Started to add Fantasy-Land methods in the "fantasy-land" branch.

Reactive streams cannot obey all FL laws while at the same time providing useful implementations – as discussed in #352 . Even libraries providing a monadic alternative to Promise – so in a way "single value reactive streams" do not adhere to the strict FL rules - as most of them provide a parallel executing ap combiner. (The standard would demand sequential execution – and thats mostly not useful)

With those FL methods (map, ap, of, chain, filter, concat) baconjs gets the first part of the functional static point-free interface for free which provide FP toolkits like Ramda (lift, pluck, …). To have that point-free interface – which is nice syntactic sugar – is the goal.

The rest of the static functions could perhaps be provided by an augment library for example like purifree-ts is for purify-ts.