rxjs: subscribing fails with a function that doesn't have a normal `bind()`
[Update: the conversation has shifted down-thread, from this OP being about a “broken” subscriber callback, to whether or not there’s any merit to being able to opt-out of bind(..)
ing, for performance reasons]
Describe the bug
I have a unique situation where I’m trying to subscribe to an observable with a function that has an overridden bind(..)
, which does something different than normal Function#bind
would do. I tried passing that function as a subscription next
function, and subscribe(..)
either seems to ignore it (silently) or in some other way just fails to ever call it as expected.
Expected behavior
I expected to be able to pass in any function that is normally callable, like fn(..)
(or fn.call(..)
or fn.apply(..)
) and have it work fine as a subscription.
As shown in the reproduction code below, the simple workaround is to wrap my function in an arrow/lambda. That’s not awful, but it’s annoying and a bit wasteful to have to do so. I’d really like to avoid that.
I wouldn’t have expected that subscriber functions must be this
-bindable. I read through some documentation and I didn’t see such a gotcha listed – apologies if this is documented and I missed it.
If it’s not documented, I think it probably should be!
My argument against the current behavior is, a lot of people probably pass =>
functions anyway, so Rx is likely not getting much out of trying to bind the this
context on those subscription function calls.
But if that behavior is just absolutely required by RxJS, I wonder if there could be some way of opting out that doesn’t involve having to wrap my function in an arrow/lambda every time? Perhaps, for example, it could be something like one of these:
rx.subscribe({ _next: myA }); // or some other variation of property name
// or
rx.subscribe({ next: myA, noBind: true });
// or
rx.subscribeNoBind({ next: myA });
It could ALSO possibly be that having a path to opt-out of this
binding – for those like me don’t need or want it – might even be just ever so slightly faster on execution. 😉
Reproduction code
import { of } from 'rxjs';
function myA(v) { console.log("a",v); }
function myB(v) { console.log("b",v); }
myA.bind = function(){ /*.. something different ..*/ }
myB.bind = function(){ /*.. something different ..*/ }
const rx = of(42);
// does not work
rx.subscribe({ next: myA });
// works fine
rx.subscribe({ next: v => myB(v) });
Reproduction URL
https://stackblitz.com/edit/rxjs-y1wqko
Version
7.5.2
Environment
Node 16
Additional context
The source of these special functions (that don’t have a normal behaving bind(..)
) is from a library I wrote, and I want users to be able to use them with RxJS as easily as possible.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 19 (12 by maintainers)
Commits related to this issue
- fix(subscribe): allows functions where bind has been patched to be weird Apparently, code exists in the wild that will patch function bind to do something other than return a function that will execu... — committed to benlesh/rxjs by benlesh 2 years ago
- fix(subscribe): allows functions where bind has been patched to be weird Apparently, code exists in the wild that will patch function bind to do something other than return a function that will execu... — committed to benlesh/rxjs by benlesh 2 years ago
- refactor(SafeSubscriber): optimize perf for ordinary observers No longer require function binding if we aren't using the deprecated next context. This should improve performance in the common path of... — committed to benlesh/rxjs by benlesh 2 years ago
- perf: removed code that would `bind` functions passed with observers to `subscribe`. (#6815) * refactor(SafeSubscriber): optimize perf for ordinary observers No longer require function binding if ... — committed to ReactiveX/rxjs by benlesh 2 years ago
- Update dependency rxjs to v7.5.4 (#1159) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [rxjs](https://rxjs.dev) ([source](https://github.com/reacti... — committed to Calciumdibromid/CaBr2 by deleted user 2 years ago
- Update dependency rxjs to v7.8.0 (#1668) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [rxjs](https://rxjs.dev) ([source](https://github.com/reacti... — committed to Calciumdibromid/CaBr2 by deleted user 2 years ago
@benlesh I’m reluctant to indulge your actual question directly, as its focus on my previous-but-now-set-aside motivations may further distract and invite bikeshedding of its own.
However, for curiosity/posterity sake, I will explain the kind of functions that led me to even discover that Rx was doing
this
binding – I never knew it did that in all my years interacting with the lib! But I’m gonna put the rest of my answer below, so it’s not as tempting to get off into the weeds any further.The way I discovered that Rx does this binding of subscribe callbacks is by providing a function to Rx that intentionally has replaced the normal
bind(..)
function with one that does a very different kind of task tothis
binding. This kind of function will never bethis
-aware and therefore has absolutely no reason forthis
-binding. It’s also a unary function, so it would never need the partial-application capabilities that built-inbind(..)
provides.So why did I override the name
bind(..)
instead of picking a different method name? There’s a very strong reason to do so. Trust me, I didn’t make the decision lightly. I know the downsides.The special kind of functions I’m talking about are created by my library: Monio. Monio is a monads-in-JS library.
As you may be aware, the concept of monads has one central capability associated with it, which often goes by one of several very common names in the FP world:
bind(..)
,chain(..)
, orflatMap(..)
. Regrettably, there’s no standard name for this behavior, so depending on your experiences, you may be used to or prefer any of those 3 (or something else entirely!) My informal surveying estimates that more than 90-95% of all monad implementations, especially in JS, use one of those 3 names.Monio chose, 2+ years ago when I first wrote it, and in an attempt to be as widely approachable as possible, to use all 3 names as aliases, so you can pick whichever your favorite name is for this core monadic behavior.
I didn’t ever anticipate at the original time of design that the monad instances created by Monio would be anything other than normal objects, so the method names I put on it were never subject to a collision issue. However, I knew that
bind(..)
andflatMap(..)
were both commonly used method names in other parts of JS, such as on functions and arrays.Well, as it so happens, a few months ago, I designed a new monad kind for Monio, and these instances of this monad kind are… functions. Not monads holding a function as a value, but literally the monad instance itself is a function.
Now of course, I have a collision issue between monadic
bind(..)
and JS function’sbind(..)
. What to do!?!?bind(..)
from all monad kinds? It’s far too late for that sort of breaking change, given that Monio has been around (and used in production) for 2+ years.bind(..)
method? That’s problematic if someone is used to using the namebind(..)
with all other monad kinds in Monio, and then for this new kind the bind(…)` method isn’t what they expect!bind(..)
method name entirely? Tempting, since these functions-as-monad-instances will never needthis
-binding or partial application. But again, creates the same problem for those usingbind(..)
on other monad kinds and not being able to usebind(..)
on this kind of monad.bind(..)
method to alias the monadic bind? Seems like the more appropriate move for Monio and its users. For completeness sake, I actually back-up the originalbind(..)
as_bind(..)
on the function, just in case in some extraordinary case some really had to have the “real” bind.OK, so option 4 is what I chose. It’s not perfect, but it’s better than (2) and (3) which create an inconsistency. And (1) is a really extreme route that I don’t wanna do.
Here’s where things get more interesting… the new monad kind I’ve invented for Monio is a special kind of monad that is basically mimicking, or acting as a companion to… observables. They’re called
IOx
(as in “Reactive IO”). In fact, I have afromObservable(..)
method on this new monad kind, which is designed to consume an Rx observable and turn it into anIOx
, and of course the other direction too,toObservable(..)
is an obvious and useful tool.My point? These
IOx
monads are actually designed and intended that people will use them interactively with Rx observables in their code base. So it’s very likely that one of them could end up being used as a subscription callback to Rx.Writing tests for my library is exactly how I found this current incompatibility. It’s obviously quite ironic that in trying to make my library more friendly to Rx users, I discovered this gotcha that makes them ultimately somewhat-less compatible.
So then I started wondering if there was some way for Rx to add a small additional opt-out feature, which would smooth over this incompatibility. As I illustrated above, it’s not really an option for Monio to rename its methods or create inconsistencies in its design.
Ultimately, if this request is denied, it’ll just make it a little more error prone for folks to use my library with Rx. That bums me out, but it’s not the end of the world.
FWIW: This issue has me thinking about a better architecture in general for
Subscriber
going forward, and I’m pretty excited about it. But I think it’s going to be something we’ll have to try to get to in version 8, not version 7.Just to re-hash in the simplest terms. Whatever
bind
calls are done in RxJS v7, are done specifically so we don’t breakthis
on supplied object methods when we try to wrap the functions to deal with unhandled errors (an RxJS feature). Basically just ensuring this works:The
this.unsubscribe()
stuff is deprecate and a red herring.However, what is being posed as a bug/issue here is something that would break with even the simplest function that relied on
Function.prototype.bind
’s behavior:So I guess what I’m going to ask, @getify, can you present a case why it’s reasonable for any library to assume that users have passed in a function with a broken
bind
method? I don’t mind the fix, honestly, I just want to make sure we’re not accommodating a corner-case we shouldn’t be.@getify FYI: the fix for this is published with 7.5.3
I actually have two different PRs for different approaches to this #6796 and #6797.
That said, this is a highly unusual ask. Basically, @getify, you’re saying “please don’t rely on the behavior of
Function.prototype.bind
because someone could patch it”. Given thatFunction
is one of the most primitive types in JavaScript, if we can’t rely on its methods, I’m not sure what we can rely on.I’m not sure what the performance implications are for either of my PRs. I feel like the second one is closest to parity with the original code, however the first one with the arrow functions will be cleaner in the 8.x branch (where we’ve done away with a deprecated behavior related to the whole bind thing).
It’s really not required for anything. It’s one approach to ensuring we don’t break what
this
means inside of each of the functions passed with the observer. Short-term one of the two PRs I have would do. Longer-term, I’d like to re-architect things in this area to help perf a little. To that end, I have some local spikes for version 8.Anyhow, this all requires careful consideration. I would rather not introduce new features, as we end up supporting them for years after. Ideally any solution would “just work” and be generally beneficial for the community.
Whether it’s documented or not, the fact that every single subscriber callback is
this
-bound to the subscriber, makes it a user-facing feature. There must be code out there that is relying on that, and will break if Rx removedbind(..)
internally for some reason.In fact, the code comment I referenced above, which talks about intentionally deprecating the access to
this.unsubscribe(..)
, is proof that this is considered a “feature”; if it was only an implementation detail, there’d be no need to deprecate it.Me too. I think this should be considered as a feature-request. But Rx isn’t my library. I don’t need to spend any more time advocating for it. If y’all decide to reject the request, c’est la vie. Sorry if I’ve been disruptive here.
Overall, unless I miss some points this request asks to change internals of library, or expose those internals of libaray into public api surface without noticeable gain we can actually achieve. I’d happy to be proven wrong if I could see 1.
bind
actually creates lot of overhead 2. Therefore, we’d like to remove it in the future version but in any case I don’t think I’ll agree to expose those into public api surface level.Other core team members might have a different opinion, but in my opinion, this is internal implementation detail of a, or any library that relies on standard behavior of javascript. Yes, as you said
bind
itself may have slightly different characteristics other than some spec behavior of fn - but that doesn’t mean any code / library should be able to provide an escape hatch if consumer intentionally choose to override its behavior to not follow intended specs. I’m a bit lost to understand why RxJS should provide an escape hatch, or explicitly explain it relies on certain spec behavior internally for these cases.It may possible further version of RxJS could remove usage of
bind
for some other reasons. If that happens, that may resolve your usecases but I don’t expect we’ll refactor code specifically because of this reason.