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

Modular Observable #29

Closed
trxcllnt opened this issue May 31, 2015 · 8 comments
Closed

Modular Observable #29

trxcllnt opened this issue May 31, 2015 · 8 comments

Comments

@trxcllnt
Copy link
Member

Many in the node community have expressed interest in an extremely modular Observable implementation. Creating this issue to gather feedback on the idea of:

  1. a bare-bones Observable implementation (Constructor and subscribe function)
  2. creation strategies and operators separated into modules
  3. operators added to the prototype as require'd

This way, the operators that rely on other operators (i.e. concat being a merge(1)) can require the modules they need, and the package manager can worry about factoring out common modules.

@Blesh @jeffbcross

@jeffbcross
Copy link
Contributor

A modular Observable would also be nice for frameworks (including Angular).

Perhaps a good approach to number 3 would be to leave it up to the developer to explicitly handle decoration (with a helper method on Observable), and operators could be written as functions that expect to be called with context, which would fit nicely within the bind_operator ES proposal.

//map.js
export function map () {
  // this == Observable instance
  return new MapObservable(this);
}
//mymodule.js
import {Observable} from 'rx';
import {map} from './map';
//Plain old call
var observable = new Observable(...);
map.call(observable, (val) => val).subscribe(...);

//Decorate Observable
var MyObservable = Observable.decorateFrom(map);
(new MyObservable(...)).map(val => val).subscribe(...);

//Bind_operator style (I think)
(new Observable(...))::map(val => val).subscribe(...);

Whatever the solution to number 3, I think it's important for the "global" Rx object to be treated as immutable, and more explicitly decorated by the user. The best practice for app developers could be to create a module with the Rx object decorated with common operators for an application, which could be further decorated as needed by other parts of the application. Distributions like rx.lite and rx.all could also use this approach to have distributions with common operators, for users who aren't as byte-sensitive.

What do you think @trxcllnt? + @vsavkin

@jeffbcross
Copy link
Contributor

My "Decorate Observable" proposal is flawed, since there are many Observable subclasses. Decorating a copy of Observable would require other implementations, like MapObservable, to know about the decorated prototype they should be inheriting.

@trxcllnt
Copy link
Member Author

trxcllnt commented Jun 1, 2015

@jeffbcross I've used the convention that the source Observable is the last argument to an operator, but the operator can default to this, for example:

// src/operators/map.js
module.exports = function map(project, source) {
  return new MapObservable(source || this, project);
};

However, this is in direct conflict with another feature I've been hoping to support: maintaining Observable subtypes through the chain. If an operator returns a subclass of Observable, subsequent operators should return the same subclass.

If we can ensure this, we won't need to save out the Observables from operators that return subtypes, like multicast, to interact with them later. It will also make subclassing and adding your own Observable operators possible.

For operators, each Observable subclass is essentially just a factory that proxies the operator arguments to the operator-specific Generator subclass. If the Observable constructor accepts source, generatorFactory, and generatorArguments, the operator can delegate Generator creation to the source Observable's constructor:

// src/Observable.js
var Generator = require("rx/Generator");
function Observable(source, generatorFactory, generatorArguments) {
  this.source = source;
  this.generatorFactory = generatorFactory;
  this.generatorArguments = generatorArguments || [];
}
Observable.prototype.constructor = Observable;
Observable.prototype.subscribe = function subscribe(_next, _throw, _return) {
  if(_next && typeof _next === "object") {
    return this._subscribe(_next);
  }
  return this._subscribe(new Generator(_next, _throw, _return);
};
Observable.prototype._subscribe = function _subscribe(generator) {
  var generator2 = this.generatorFactory.apply(generator, this.generatorArguments);
  var subscription = this.source.subscribe(generator2);
  switch(typeof subscription) {
    case "function":
        return Generator.return(subscription);
    case "object":
        return subscription || Generator.empty();
    default:
        return Generator.empty();
  }
}
// src/operators/map.js
var Generator = require("rx/Generator");

function MapGeneratorFactory(project) {
  return new MapGenerator(this, project);
}

function MapGenerator(destination, project) {
  this.project = project;
  Generator.call(this, destination);
}

MapGenerator.prototype = Object.create(Generator.prototype);

MapGenerator.prototype._next = function _next(value) {
  return this.destination.next(this.project(value));
}

module.exports = function map(project, source) {
  return new (source || (source = this)).constructor(source, MapGeneratorFactory, [project]);
}

@benlesh
Copy link
Member

benlesh commented Jun 1, 2015

I've used the convention that the source Observable is the last argument to an operator

I have (perhaps unfounded) concerns about adding too many optional arguments to our methods, and how that will affect the optimization of the library. It seems like in doing so we'll be creating a lot of polymorphic functions. That might be okay, if they all have monomorphic functions backing them, but that could end up bloating the library too.

Food for thought.

@benlesh
Copy link
Member

benlesh commented Jun 12, 2015

I think we've got a good approach for this at the moment.

Right now we have each operator in it's own module, so if you're using a build system or a module system, you can reference those directly and get the desired effect. Also, I'm a big fan of the ES function bind proposal that @jeffbcross pointed out. It's already available in Babel, and hopefully the TypeScript people get on the ball with that. We don't need it, of course.

@benlesh benlesh closed this as completed Jun 12, 2015
@calebboyd
Copy link
Contributor

@trxcllnt I'd be interested in knowing if you are still pursuing the idea of maintaining Observable subtypes through the chain or if it has been dropped. I dabbled with this idea using lodash's chaining mechanism but I don't think it was ready to be externalized.

@benlesh
Copy link
Member

benlesh commented Jun 22, 2015

@calebboyd it has been dropped for now. There really isn't any prior art for maintaining a custom type through a fluent method chain. The ES7 function bind feature is more than enough to help most people's needs for chaining custom functions.

The major thing that killed this feature is that once native Observables land in-browser, ideally this whole library becomes a set of extension methods built on top of that. As people have learned with Promises, the in-browser Promise debugger only works with native Promises, and not Promises from libraries. Future tooling for Observable is likely to be the same.

This library is still pre-alpha though, so if consensus changes, we'll change it back.

@calebboyd
Copy link
Contributor

Thanks @Blesh for replying. I agree the :: syntax is convenient. I almost wish there was something explicit like that for other language's extension methods (C#).

As for prior art, I'm not sure this pattern exists in JS, but Scala supports similar behavior with this.type (mutable chains) and cough* Java, also has co-variant return types.

-- I'll keep watching, exited to see where this library is headed.

@lock lock bot locked as resolved and limited conversation to collaborators Jun 8, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants