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

"Observable is a glorified function" is confusing framing #6

Closed
domenic opened this issue Mar 27, 2023 · 1 comment · Fixed by #15
Closed

"Observable is a glorified function" is confusing framing #6

domenic opened this issue Mar 27, 2023 · 1 comment · Fixed by #15

Comments

@domenic
Copy link
Collaborator

domenic commented Mar 27, 2023

Maybe there is some abstract sense in which this is true, but it makes no sense to me. The natural followup questions with this framing are:

  • Can it be constructed, or just called?

  • Is it an async function, a generator function, a plain function...?

  • Why do we need these when we already have functions?

And then

Additionally, it has extra "safety" by telling the user exactly when:

just makes it seem like some of the most important features of observables, don't even fit into this model?


I would suggest describing how observables work, independent of any analogy with functions. IIUC the main points to communicate are:

  • The creator of the observable can signal zero or more values, followed by either one "complete" signal or one "error" signal. The error signal comes with an error value, which is traditionally a subclass of Error.

  • The creator's code to generate this sequence-of-values is called lazily, meaning, it happens once subscribe() is called.

  • The creator's code to generate this sequence of values is called potentially multiple times, whenever subscribe() is called.

I would then very quickly move into making this concrete via events, possibly with code examples. I.e., you can call subscribe() as many times as you want, and every time, it calls "the creator's code" which subscribes to the EventTarget. (Using the same mechanism, under the hood, as addEventListener() does.) This makes it more clear why the above properties are desirable properties, for practical purposes. (Although you need to do a bit more work to explain the complete and error signals.)

@benlesh
Copy link
Collaborator

benlesh commented Apr 6, 2023

The context around the statement, step by step:

  1. You can write a function that does something similar:
// An observable-ish function
function ticker(observer) {
  let n = 0;
  
  const id = setInterval(() => {
    observer.next(n++)
    if (n === 5) {
      observer.complete();
      
      // developer mistake here
      observer.next('oops')
    }
  }, 1000);
  
  return () => {
    clearInterval(id);
  }
}

// usage

const unsub = ticker({
  next: console.log,
  complete: () => console.log('done')
})

// unsubscribing later (optional)
setTimeout(() => {
   unsub();
}, 60000);

This seems fine, but unfortunately there are a lot of bugs that aren't immediately obvious to the developer.

  1. There's calling observer.next() after observer.complete(). That shouldn't do anything, and it will.
  2. Worse: When the thing completes, the interval will keep ticking along until the external provider unsubscribes by calling the returned function.

To resolve these issues, we have to take the SAME function and wrap it in something that will internally provide these guarantees:

// It's the same function, but we've wrapped it in a class
// that will take the passed observer and wrap it in a "subscriber"
// that does a few things:
// 1. Ensures safety around calling `next`, `complete`, or `error` after everything is finalized.
// 2. Links the subscription teardown to the calling of `complete` or `error` to guarantee cleanup.
// 3. Allows partial observers to be passed (each handler is optional).
const ticker = new Observable((subscriber) => {
  let n = 0;
  
  const id = setInterval(() => {
    subscriber.next(n++)
    if (n === 5) {
      subscriber.complete();
      
      // developer mistake here
      subscriber.next('oops')
    }
  }, 1000);
  
  return () => {
    clearInterval(id);
  }
});

// usage
// Subscribe just immediately calls the function with the
// subscriber that wraps the passed observer.
const subscription = ticker.subscribe({
  next: console.log,
  complete: () => console.log('done')
})

// unsubscribing later (optional)
setTimeout(() => {
   subscription.unsubscribe();
}, 60000);

that's really it in a nutshell.

(I'm NOT proposing this, just stating it for perspective...) If we created some JS-language level syntax for this, it could even look something like a generator function. function# or the like. It's just instead of returning a generator/iterator, you'd get an observable.

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

Successfully merging a pull request may close this issue.

2 participants