Skip to content
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

Bacon.js 4.0 #771

Open
raimohanska opened this issue Aug 21, 2020 · 14 comments
Open

Bacon.js 4.0 #771

raimohanska opened this issue Aug 21, 2020 · 14 comments

Comments

@raimohanska
Copy link
Contributor

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

@raimohanska
Copy link
Contributor Author

Property reactivation alternative is something to consider: #770

@raimohanska
Copy link
Contributor Author

"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
Copy link
Contributor Author

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
Copy link
Member

semmel commented Aug 23, 2020

"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

  • take the observable as last argument,
  • are auto-curried least changing argument first,
  • are of fixed-arity

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
Copy link
Contributor Author

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
Copy link
Member

semmel commented Aug 25, 2020

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
Copy link
Contributor Author

...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
Copy link
Member

semmel commented Aug 25, 2020

@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
Copy link

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
Copy link

  • Remove Property and just have EventStream.
  • Consider merging EventStream and Observable.
  • Replace toProperty with an operator called remember.
  • Replace toEventStream with an operator called forget.

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
Copy link

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
Copy link

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:

  •  function toArray<VALUE>(): Collector<VALUE, VALUE[]>
  •  function toFirst<VALUE>(): Collector<VALUE, VALUE>
  •  function toLast<VALUE>(): Collector<VALUE, VALUE>
  •  function toSet<VALUE>(): Collector<VALUE, Set<VALUE>>
  •  function toMap<FROM_VALUE, TO_KEY, TO_VALUE>(
         keyMapper: (value: FROM_VALUE) => TO_KEY,
         valueMapper?: (value: FROM_VALUE) => TO_VALUE
     ): Collector<FROM_VALUE, Map<TO_KEY, TO_VALUE>>

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
Copy link
Contributor Author

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
Copy link
Member

semmel commented Aug 12, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants