kefirjs / kefir

A Reactive Programming library for JavaScript
https://kefirjs.github.io/kefir/
MIT License
1.87k stars 97 forks source link

enhancement: "sensible" defaults #202

Closed boneskull closed 8 years ago

boneskull commented 8 years ago

For example, take Kefir.pool():

const pool = Kefir.pool();

The following obviously fails:

pool.plug('foo');

Would it be sensible, if an observable was not passed, to wrap it in one? e.g.

pool.plug(Kefir.constant('foo'));
rpominov commented 8 years ago

Good question! Generally I'm against this kind of APIs for several reasons:

  1. Even so JS dynamically typed I like to think about APIs with types in mind. And it will be hard to define type of this method if it'll work like this. We could try something like (Observable | any) -> void, but any (any type) includes Observable as well, so we can try (any) -> void, but this is also not good because we treat some values differently than others. A type with any is good when all values treated exactly the same (for a function like x => [x]). So it somewhat problematic from types perspective.
  2. Secondly this increases surface area of the API, so users of the library have to be aware of this feature of this method in order to use the library, along with other features of other methods etc. This stuff adds up. Sebastian Markbage: Minimal API Surface Area | JSConf EU 2014
  3. Finally keeping API minimal allows us to extend it over time without breaking changes.

I think these are good reasons for having to write a bit of boilerplate code.

boneskull commented 8 years ago

I understand your first point about "types" up to this (though I'm unfamiliar with the "typing syntax" you're using):

A type with any is good when all values treated exactly the same

Can you provide an example of why it's "not good" if a function accepts any parameter, but treats a parameter of a certain type differently?

Off the top of my head, see Array.prototype.concat(), which behaves differently if the first parameter is an Array, but can accept any value.

But, as you said, JS is dynamically typed--using instanceof to check if an Object "is an" Observable can/should be avoided. If an Object can fulfill the contract (whatever that is) of an Observable, then no coercion should be necessary.


Regarding the second point: It'd be expensive to add this behavior to plug() only, because then it'd behave differently than other methods in Kefir.

However, if Kefir's behavior was to coerce a non-Observable into an Observable whenever it needed one, I'd wager the cognitive overhead would be slim.


So you know why I made this issue:

It's really all about balance. That "surface area" talk seems like a knee-jerk reaction (excuse the pun) to over-abstracted, "magic" APIs. It seems like he's encouraging the other extreme, which is just as painful (for different reasons).

So yeah, I'm sitting on the other end of the see-saw here.

rpominov commented 8 years ago

Off the top of my head, see Array.prototype.concat(), which behaves differently if the first parameter is an Array, but can accept any value.

concat() is a great example actually. I always avoid this shortcut. Suppose we have a code like this:

function foo(x) {
  ...
  const baz = whatever(x)
  ...
  return bar.concat(baz)
}

This code will work fine until baz happen to be an array. API like that just force programmers to be more careful, and keep more info about the program in their heads in order to modify the program.

Regarding the second point: It'd be expensive to add this behavior to plug() only, because then it'd behave differently than other methods in Kefir.

Right, but if we make that change through all API, this will decrease drastically flexibility of API to further changes (third point). For example now we can safely extend plug method like this:

(Observable) -> void              // current signature
(Number, Observable) -> void      // new additionally supported signature

Don't know what we might want to pass as a number as first argument, but we just have ability to safely extend API like that, which is great.

I want to minimize the potential for uncaught exceptions; they are not friendly to consumers.

But wouldn't we just hide potential bugs this way? I mean if programmer thought they pass an Observable to plug, but because of a bug it happen to be null, and Kefir will just silently accept that value. So it even might seem to work as expected for some time, but then break in completely different place.

I'm all for balance too, and am trying to find a good balance in Kefir.

rpominov commented 8 years ago

Probably won't happen closing for now to cleanup open issues.