Skip to content

Going Super-Lazy... #8701

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

Closed
electricessence opened this issue May 19, 2016 · 21 comments
Closed

Going Super-Lazy... #8701

electricessence opened this issue May 19, 2016 · 21 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@electricessence
Copy link

electricessence commented May 19, 2016

So I was thinking last night about how async/await is coming to ES5 via TS2.0. I'm super excited for this. But then it dawned on me... Putting aside code that doesn't run in the browser. Why can't we have a TS compiler that has a "superLazy":true mode?

What is Super-Lazy?

So you can write code for JS that takes dependency management a step further. Instead of simply:

// Preferably this is how super lazy code would look without even you even knowing it.
var myObj = new ModuleClass();
myObject.doSomething();

could potentially write it:

// If not just looking like regular synchronous code...
var myObj = await new ModuleClass();
myObject.doSomething();

which either would really equal:

require("ModuleClass").then(ModuleClass=>{
   var myObj = new ModuleClass();
   myObject.doSomething();
});

Instead of forcing the developer to write more async code, the TS compiler could add the code that uses the async/await pattern automatically.

Why?

Because we build classes and libraries that link to each other but sometimes in our lazy world of code that particular dependency never gets used! So why load the dependencies ahead of time? Why not defer them to the last second just like an AsyncLazy<T> (aka Promise<T>) class or something?

@electricessence
Copy link
Author

This may not be necessary for everything but if you can imagine the chain of dependencies that may be avoided if a particular class is never instantiated even though it's imported.

@electricessence
Copy link
Author

So here's my real world example:
Just like .NET I have an IEnumerable<T> interface an subsequent classes that follow that pattern.
I also have LINQ extensions by loading a module and calling Enumerable.from(myCollection).

The problem is, the LINQ library is _huge_. And although I would love for every class that implements IEnumerable<T> to have the full LINQ functionality, I then automatically incur that overhead even if per-se someone was to use Queue<T> but only for its basic functionality and never use LINQ.

So my plan is/was to create an .asEnumerable():IPromise<ILinq> or something to the effect.
Which under the hood would:

asEnumerable():IPromise<ILinq> {
  return require("../System.Linq/Linq")
      .then(Enumerable=>Enumerable.from(this));
}

Although the above fits what I'm trying to accomplish, it's basically clunky and not ideal.
Ideally, I would simply want it to work like:

var names = myIEnumerable.linq // where .linq is accesor that returns a promise that may have already resolved.
    .select(person=>person.name);

Is this making sense?

@mhegazy
Copy link
Contributor

mhegazy commented May 20, 2016

I do not think TS should be hiding that much details from the user. Moreover, most of the value outlined above can be achieved by structuring your project into units ( be it 1 class, multiple classes,..) and having every unit in a module.

One other thing, this would break single file compilation, as the compiler needs to know were to get the class from in the first place, which relies on global program information.

@mhegazy
Copy link
Contributor

mhegazy commented May 20, 2016

related discussion in #2508

@DaSchTour
Copy link

Isn't SystemJS already doing this?

@tinganho
Copy link
Contributor

tinganho commented May 20, 2016

@electricessence I'm pretty sure the ES module loader spec will cover your async module loading case.

Here is some related issues I filed before:
#3100.
whatwg/loader#59

@mhegazy mhegazy added Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript labels May 20, 2016
@mhegazy
Copy link
Contributor

mhegazy commented May 20, 2016

closing as out of scope of the TS project at the time being.

@mhegazy mhegazy closed this as completed May 20, 2016
@electricessence
Copy link
Author

@mhegazy ...

One other thing, this would break single file compilation, as the compiler needs to know were to get the class from in the first place, which relies on global program information.

Would not break it. You would still use the same import statements. The code would read exactly as you would normally write synchronous code. But the require would happen at instantiation of the desired class and not the host class.

@electricessence
Copy link
Author

So again, there is a way to manually do this. It's just the nature of TS and it's ability of async/await rewriting that allows fro this to happen. I still hope people think about it or how to enable it.

@electricessence
Copy link
Author

electricessence commented May 20, 2016

@DaSchTour

Isn't SystemJS already doing this?

As far as I know, there is no loader that does this. The code would have to be interpreted/re-compiled to allow for pure async loading.

@mhegazy
Copy link
Contributor

mhegazy commented May 20, 2016

there is no way to do this correctly unless you 1. do global analysis and know which classes are created asynchronously and which are not, or 2. rewrite every construct call to an asynchronous operation. and in both cases, the cost of rewriting a all your new's to async, and then transforming the rest of your code is substantial. I do not think there is much value here to be frank to do this all the time for all classes and all instantiations. you are much better off identifying what are units of code that should live together, and importing them when needed. then use a bundler (or just tsc --outFile on your entry module) and that will do tree shaking for you.

@electricessence
Copy link
Author

So the obvious point is that if you are bundling or 'outFile' then this is a useless concept for you.
But for me, I have a perfect use case that makes sense to me. I don't want to (but may end up) rewrite my universe of code to be async everywhere, but it solves a serious problem...

Here's my example...

Let's say you have an interface like IEnumerator<T>.
But for every type of stream, iterator, collection, generator, or anything imaginable, each will have a unique/specialized/optimized enumerator that works with it.

Now let's say you have a method called

export function from(e:any):IEnumerator<T> { /* complex logic to select the right class */ }

At the head of this file is a huge number of imports for different enumerators.
But for the life of the application, you may only use 1 of 5 of these even though you loaded all of them.

Bundling doesn't solve this issue either. It just incurs the unnecessary cost up front.

@electricessence
Copy link
Author

electricessence commented May 21, 2016

@mhegazy and sorry, just to clarify... I don't see what I'm suggesting as anything different to what the async/await rewrite does. Simply stated, your imports declare which potential classes you might use, and in place of their constructor, you have some async function.

It would not require global analysis.

@kitsonk
Copy link
Contributor

kitsonk commented May 21, 2016

I don't see what I'm suggesting as anything different to what the async/await rewrite does.

But this is based on what is reasonably expected to be agreed as a standard for the ECMAScript language. The place to start for this would be TC39 or more likely the WHATWG for the System Loader.

@mhegazy
Copy link
Contributor

mhegazy commented May 21, 2016

For this case you can isolate enumeration in one module, then use conditional loading(require, or System.import, ...) and await the promise.

@electricessence
Copy link
Author

Let me try and explain how you would need to do this effectively today (which isn't too bad if it's just one thing) and it will be obvious then how async/await helps. And what I'm suggesting is simply allowing a rewrite (maybe I need to get my hands dirty in the TSC) that replaces new X with new require("X") where the import is skipped at the beginning. And please help me by showing me where I'm wrong here.

Also note, I understand that this is not a concern for most. It only really applies to browser/web loading and not NodeJS/CommonJS. But if I was to create a UMD module I would want this.

If I need to do what I'm talking about today and still retain type information...

  • I would need to define the entire interface of the class I was instantiating so the typing could be reflected. Please use your imagination here as it gets a bit crazy if the class is larger and hence why you would want/need to do this in the first place.
// IMyHugeUtility.d.ts
export interface IMyHugeUtility{
  /// ... endless methods inside ...
}
  • Inherit from that interface and expose the file MyHugeUtility.ts.
  • Consume the file.
myMethod():Promise<IMyHugeUtility> {
  var r = Promise.pending();  // Because require (AMD) doesn't inherently return promises :/
  require("./path/MyHugeUtility",(MyHugeUtility:IMyHugeUtility)=>{
     r.resolve(new IMyHugeUtility());
  });
  return r;
}

And subsequently... Putting async in front of this method then makes it nice to consume in a synchronous way.

@kitsonk
Copy link
Contributor

kitsonk commented May 23, 2016

What you are talking about is supporting one very specific pattern at the cost of increasing the complexity of the compiler, adding compiler behaviour that isn't based on any existing or planned ES language feature and could easily "surprise" many developers, all for the cost of not being explicit about dependencies. This seems like a case of the fallacy of composition. How would you do with namespace resolution, where there are conflicting names? What if someone has a class name that conflicts with a built in (e.g. Bluebird's Promise)?

@electricessence
Copy link
Author

electricessence commented May 23, 2016

@kitsonk I agree that this does seem like an edge case. And in no way am I assuming it's easy.
But not only do I believe in the value of it, I also believe that the TSC could be the only thing that could do it right! Only a compiler with async/await capabilities would be able to do this effectively.

"Namespace resolution": How is this even an issue?

You would still write your code as normal including your imports.

import MyHugeClass from './MyHugeClass';
import {myFunc} from './aLotOfCode';
import {somethingThatRunsImmediately} from './overHere';

/* The bulk of your code. */

somethingThatRunsImmediately();

// Not sure how much of the async portion of this pattern would be required.
// (just one example here)
async function somethingThatMayNeverBeCalledFor():Promise<MyClass>
{
    var n = new MyClass();
    myFunc(n);
    return n;
}

/* some more code */

So the compiler would treat the TS as the same code with resolution as normal.
The difference is, setting a superLazy:true flag would automatically 'asynchronize' any imports.

So the resultant (pseudo) JS code would be:

// Note: no actual imports here.
define(["require", "exports", "somethingThatRunsImmediately"],
function(somethingThatRunsImmediately)
{
    /* The bulk of your code. */

    somethingThatRunsImmediately();

    function somethingThatMayNeverBeCalledFor()
    {
        return new Promise(resolve=>
        {
            require("./MyClass", MyClass=>
            {
                var n = MyClass();
                // same pattern for myFunc
                resolve(n);
            });
        });
    }

    /* some more code */
});
});

Sorry if that seems either repetitive or long winded.

What would be a good first step if I wanted to contribute to TypeScript and potentially implement this feature?

@mhegazy
Copy link
Contributor

mhegazy commented May 23, 2016

As noted earlier this feature is out of scope of the TS project at the time being.

@electricessence
Copy link
Author

@mhegazy noted

@electricessence
Copy link
Author

@kitsonk (and everyone else). Thanks for the discussion. This actually helped me move forward with a CommonJS solution. Still not perfect, and I still need to import a huge number of declarations, but it will work well enough for now.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants