kosich / rxjs-autorun

Re-evaluate an expression whenever Observable in it emits
MIT License
34 stars 2 forks source link

Maybe default subscription strength should be "Strong" instead of "Normal"? #30

Closed loreanvictor closed 1 year ago

loreanvictor commented 3 years ago

Right now, for this simplistic example:

import { computed, $ } from 'rxjs-autorun';
import { interval } from 'rxjs';

const a = interval(1000);
const b = interval(500);

computed(() => $(a) % 2 === 0 ? 'A:' + $(a) : 'B:' + $(b))
.subscribe(console.log);

You get this output:

A:0
B:0
A:2
B:0
A:4
B:0
A:6
B:0
...

Playground

This means even for pretty simplistic cases users would need to take subscription strength (and the fact that their observables might regularly get unsubscribed) into account. So if the intent is to sort of hide that from users and only keep it as an option for advanced usage, I would recommend bumping default subscription strength.

P.S. to be fair the default strong output is also not that super intuitive:

A:0
B:0
A:2
B:2
B:3
B:4
A:4
B:6
B:7
B:8
A:6
B:10
B:11
B:12
...

But I suspect you can reason about it without knowledge of subscription strength. It is also notable that this scenario only happens in conditional expressions.

Jopie64 commented 3 years ago

I argued for weak subscription by default. See #7. The 'normal' semantics are more or less a compromise 😊 The biggest reasons for me to argue about weak were:

So when you actually want strong subscription, I opted for an API change that would allow you to sum up the observables you want to subscribe strongly to. This way the strongly subscribed observables will also be subscribed immediately (avoiding late subscription), and the compute function will not run before all strong observables emitted at least once (avoiding midflight abort). See this comment (when the compute function was still called run) https://github.com/kosich/rxjs-autorun/issues/7#issuecomment-704294658

We postponed this idea, but I think now it's a good time to reconsider it 😊

I'm on holiday this week, no access to computer so maybe a bit unresponsive 😁

loreanvictor commented 3 years ago

I'm on holiday this week, no access to computer so maybe a bit unresponsive 😁

Always nice to meet fellow ossaholics 😂

I fully understand the value of fine-grained subscription management. However, I feel the main benefit of this library (specifically the notation it provides) is that it masks inherent complexities of working with observables (the pipe notation is unfamiliar to most and not particularly tree-friendly, and there are lots of considerations for simplest actions such as combining multiple observable values which rightfully turn people not familiar enough with the library / concepts away).

On that note, I feel it should not by default expose additional complexities to the very same users (and for the very same use-cases) that it provided simplicity and intuitiveness. Simply put: I feel good API design means when you know the minimum necessary basics you can intuitively understand (and reason about) behavior of code only utilizing those basics, without being required to learn more. In this case, I believe subscription strength should not be considered amongst the minimum necessary basics of this library, and so it should not affect behavior of code that is not explicitly invoking it.

kosich commented 3 years ago

Hey, fellas! 🙂

@loreanvictor , I totally understand where you're coming from: we had a really long conversation with @Jopie64 the other day about this (#7), with me promoting strong by default. Back then I was thinking of only two options: strong and normal. And when Johan introduced me to the idea of weak type — the discussion restarted twice as hard (#10) 😅

but don't waste your time re-reading all that

I do agree that in many cases strong is the desired behavior (for cold observables with cheap subscriptions) Though my original expectation was that the library would be primarily used with BehaviorSubjects (~hot & cheap), where it doesn't really matter which strength is used. At the same time, I'd say that fromEvent/websocket/etc (hot & expensive) are expected to be dropped if unused. Especially in cases where we create or reuse observables on the fly, I'd expect it to be normal (or weak?):

computed(() => {
  const url = $(a);
  return $(cacheOrFetch(url))
});

// memoized fn
function cacheOrFetch(url) {…}

(also Johan gives a great example where weak is desired)

--

So as you guys can see, given the right case and user level, we can argue for any strength to be the default one. My current thinking is that when user hits an issue with subscriptions — they gotta learn about subscription & strength. That's great that we at least provide "normal", "weak" and "strong" as options. And this is something we can't really cover under our smart "autorun" abstraction: subscriptions are a too important feature of Rx land.

For me, at the moment, normal seems to be the safe middle ground:

  1. no memory leaks
  2. no unexpected eager disconnects/reconnects

What we probably can improve is documentation & explanation of subscription nuances.

loreanvictor commented 3 years ago

And this is something we can't really cover under our smart "autorun" abstraction: subscriptions are a too important feature of Rx land.

So if this is the conclusion, then the only important factor for default strength would be convenience, in which case normal seems like a proper choice (at least until further usage data is available).

On a closing note though, I would like to mention that this (approach, not specific choice of default strength) does decrease learnability of the library. My hope for rxjs-autorun is more of a learnable for newcomers to Rx kind of thing vs convenient for people fluent in Rx.

RxJS itself is intentionally designed in such a way that it partially masks the concept of subscription and need for subscription management, and having observed and helped with learning of quite few people in Rx land, it seems to me that people start properly utilizing it (of course, with a minor chance of memory leaks) before (sometimes well before) they are required to learn about subscriptions (as it is also typically used in contexts that subscription management is done automatically).

From what I've seen (for example in RxJS gitter room) combination of observables is the point that people struggle with mostly when starting to dip into Rx world, and rxjs-autorun neatly resolves that issue by simply making a choice (combine latest with support for late subscription) for combination scenarios that these users are accustomed to (i.e. expressions, a + b).

However, if rxjs-autorun does in turn require new-comer users to grasp not only the concept of subscriptions, but that of subscription strength (which is fairly unique to this particular library because it arises from its design style), that added learnability is mostly gone.

CLARIFICATION: This is not further argument in favor of changing the default subscription strength. I really do not have a strong opinion on that matter either way, since for example you could argue that the unintuitive behavior in mentioned example actually isn't related to subscription strength, but rather to the fact that interval() looks like a hot observable but is not. This is just contemplation on how learnability of rxjs-autorun might be affected by subscription strength. On that front though, I would recommend generally trying to keep rxjs-autorun pretty usable for simple/common cases without requiring knowledge of subscription strength.

kosich commented 3 years ago

I didn't mean to sound overly confident or ultimative here. I too have doubts which strength is best to expose as default.

And I do share your concern about the rx-newcomers friendliness. In my mind combinatory abilities is the selling feature. While experienced users can also benefit from the library, especially if they learn strength feature (but as you rightly point, Eugene, it is very library-specific thing and might be tricky even to experienced Rxers. It is for me at least).

My current thinking is that unexpected output is easier to be traced than unseen leaking subscriptions. "Strong" needs a good understanding of the expression lifecycle. And "weak" is specially designed for experienced devs to tweak expensive subscriptions.

Therefore "normal" strength seems to me the safest for newcomers at the moment. IMHO.

--

And I'd like to state this again: this is an unmapped land we're exploring with this library (which is a big fun). And decisions we make here were not engraved in stone. These decisions we make are based on how we, experienced Rxers, imagine the library could be used by end users, whose level we don't really know. Before we see actual use-cases and while we're in early 0.0.x versions, I think, our API should be concies and can be a subject to a change of any magnitude.

(btw, regarding our imagination: filtering and switchMap are two features that we didn't really intend to create, they came up accidentally 😮)

loreanvictor commented 1 year ago

So 2 years and tons of tinkering later, I have some different thoughts on this issue:

Now I should note that in my tinkering and experiments I typically found managing subscriptions as one of the main culprits for any performance hits you would get for using reactive expressions, and one way I found for fixing it was simply unsubscribing from a source when it triggers a rerun of the expression but is not visited during that run. This almost yields the same behaviour as current normal strength, though it results in uncleaned subscriptions for sources that don't emit but also don't end. Also, I am not sure how much performance is a concern for rxjs-autorun, since it is a layer on top of rxjs, which already isn't the fastest option out there.

kosich commented 1 year ago

@loreanvictor , yeah, main reason for rxjs-autorun is to have flat code for reactive sources. Therefore I like your idea of supporting async/await #34 that pushes it even further (not a simple thing though, more thoughts in that issue) and your cool experiment with quel!

I think we (community) still have something to say in the "flat reactive", and I think even experienced devs can benefit from some code being simple and flat. This is where weak / normal / strong comes — it's rather not for newcomers, but for experienced devs who wants simple subscription management. Still I see this lib is mostly of "academic" interest for experienced Rx-ers, rather then for open public use in their day-to-day projects 🤷 🙂