Open raimohanska opened 3 years ago
Property reactivation alternative is something to consider: #770
"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.
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.
… 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.
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.
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?
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.
...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.
@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)
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 EventStream
s 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
.
Property
and just have EventStream
.EventStream
and Observable
.toProperty
with an operator called remember
.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.
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.
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 ) |
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 :)
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.
Placeholder issue for all things that might be included in 4.0. Post your ideas and vote with emojies. Knock yourself out!