amdjs / amdjs-api

Houses the Asynchronous Module Definition API
https://groups.google.com/group/amd-implement
4.31k stars 499 forks source link

Delayed definition of modules (asynchronous exports) #17

Closed ghost closed 10 years ago

ghost commented 10 years ago

AMD is not really asynchronous

The only thing which is asynchronous is the loading of dependencies.


But the definition itself is not really asynchronous. According to the specification the module is defined when factory function is executed and exports is detected.


Bad example: A module depends on a module which provides only a method to call your callback on a delayed ready state, but your module also wants to provide a ready state.

And what if an other module depends on my example module? We will run in a crazy chain of such modules ...

define(["domReady"], function (domReady) {
    var isReady = false,
        cbStack = [];

    domReady(function () {
        isReady = true;
        while (cbStack.length >= 1) {
            (cbStack.unshift())();
        }
    });

    return function (callback) {
        if (isReady) {
            callback();
        } else {
           cbStack.push(callback);
        }
    });
});

Asynchronous definition

So the module should be able to say: I'm done.

The defined status of a module should be asynchronous according to the factory function.


The idea of an asynchronous definition is, to delay the detection of exports. If the detection of exports is done, the definition of the module is done too.


Two known implementations

Currently there are 2 pull requests on RequireJS with implementations of asynchronous exports. Both are closed because we should discuss this on the amdjs-api first.


@Demurgos - #1078 - A new distinct resolution dependency named delay

define(["delay"], function (delay) {
    var Module = {};

    setTimeout(function () {
        delay(Module);
    }, 1000);
});

pro:

contra:


@evanvosberg - #1075 - A new property on the module dependency

define(["module"], function (module) {
    var asynchronousExports = module.async(),
        Module = {};

    setTimeout(function () {
        asynchronousExports(Module);
    }, 1000);
});

pro:

contra:

If factory is a function:

If factory is an object

Additional property on the define.amd object. Name should be discussed, here is one suggestion.

define.amd = {
    async: true
};
ghost commented 10 years ago

"@Demurgos commented on his pull request"

Thanks, I have seen your interesting implementation too. This was my first pull request I made so sorry, I see it was not complete : I just wanted to propose a way to deal with some rare but existing cases because what you have done is just great.

I'll try to improve my solution because it's still has its issue that it requires a handler name - personnally that's already the reason why I did not named that "async". Could it be usefull to provide some concrete examples or situations when this async definition is helpfull?

An other thing that bugged me : should the Module offer a synchronous AND an asynchronous export ? I feel like its getting to over-complicate it, could this be usefull ?

See the example above using domReady, there is an useful example of an async definition.

As described in the priority of different exports, we should keep working all existing variations of exports. I think it's not a good idea to kill some ways of exports just because the module definition is asynchronous.

Here are some examples for different ways of exports.

define(["module"], function (module) {
    var Module = {};

    doSomethingAsyncWithCallback(module.async());

    return Module;
});
define(["module"], function (module) {
    var Module = {},
        asyncExports = module.async();

    doSomethingAsyncWithCallback(function () {
        Module.addProperty = "async done";

        asyncExports();
    });

    return Module;
});
define(["module"], function (module) {
    var Module = {},
        asyncExports = module.async();

    doSomethingAsyncWithCallback(function () {
        Module.addProperty = "async done";

        asyncExports(Module);
    });
});
tbranyen commented 10 years ago

Why are you against reducing complexity by virtue of removing export methods? IMO If you opt into Simplified CommonJS, you should never be able to return a value. And vice-versa, if you haven't opt'd into SCJS, requiring module or exports should attempt to fetch module.js or exports.js. Lets try and reduce complexity, it's a barrier and a negative against AMD at the moment.

ghost commented 10 years ago

I'm not agains reducing complexity at all. Maybe it's a good idea to reduce complexity by removing some exports methods, but it's not the intention of this issue.

The issue is about to enable a delayed define event of a module, this effects the export as well. But just because of that, the module should not work completely different than all other modules. If we think about to reduce complexity by removing some exports methods, than we should think about that for all modules, not just for modules with an asynchronous define event.

Quoted from AMD specification

This specification defines three special dependency names that have a distinct resolution. If the value of "require", "exports", or "module" appear in the dependency list, the argument should be resolved to the corresponding free variable as defined by the CommonJS modules specification.

I just suggest not to change the behavior of exports and module.exports because of allowing an delayed define.

Quoted from AMD specification

If the factory function returns a value (an object, function, or any value that coerces to true), then that value should be assigned as the exported value for the module.

This already has a higher priority and I just suggest not to change this because of allowing an delayed define.

jrburke commented 10 years ago

I left a quick comment in the requirejs ticket, but to summarize and expand on it here:

I tend to want to view the module system as a way to reference and load pieces of functionality, in a way that can be traced mostly with static analysis. Instead of for example needing to define and load all functions used in a program and to refer to those functions via language identifiers, string names are used for referring to the units of functionality, and there is a way for the the loader to load those units before they are needed.

I feel allowing async exports starts to get too far away from that and into the real of application programming, as a way to deal with async APIs. I feel exports that deal with promises or generators are the more appropriate language pieces to deal with async application code, and the module system should focus on how to load and address units code.

I also think the async imports open up the possibility to not be able to correctly statically analyze dependencies, as the async part could do more require([]) calls that could not be seen by the loader or dependency tracer.

Loader plugins do allow this kind of behavior, but the contract there is that they should be coded to be runnable in a non-browser environment, and are external actors on the modules. This sort of API that is used internal to a module makes that very difficult since it is more likely those module bodies will use a specific JS env APIs, like document or window, and so those modules cannot be made to generically run in tooling.

Looking towards ES6, I do not see how an async export will be compatible with how it is constructed so far. The imports are mutable slots to allow execution of modules while having a reference to the exports of another module to allow cycles to complete. This implies to me that a module body will execute before the imports are fully formed -- the module bodies are not held for exports to complete, as it would in an async export sort of model.

I will post a pointer on the amd-implement list to this issue so others will have a chance to see and consider it. This issue list is a relatively new thing so I want to be sure others comment.

unscriptable commented 10 years ago

I was just about to comment about ES6, but I see James has already. Allowing async exports will cause problems with forward compatibility with ES6 which does not allow async factory functions.

I also agree that asynchrony is largely an application concern. With a proper IOC container such as wire.js or when using promises, the async app problems go away.

Regards,

-- John

Sent from planet earth

On Apr 7, 2014, at 1:59 AM, James Burke notifications@github.com wrote:

I left a quick comment in the requirejs ticket, but to summarize and expand on it here:

I tend to want to view the module system as a way to reference and load pieces of functionality, in a way that can be traced mostly with static analysis. Instead of for example needing to define and load all functions used in a program and to refer to those functions via language identifiers, string names are used for referring to the units of functionality, and there is a way for the the loader to load those units before they are needed.

I feel allowing async exports starts to get too far away from that and into the real of application programming, as a way to deal with async APIs. I feel exports that deal with promises or generators are the more appropriate language pieces to deal with async application code, and the module system should focus on how to load and address units code.

I also think the async imports open up the possibility to not be able to correctly statically analyze dependencies, as the async part could do more require([]) calls that could not be seen by the loader or dependency tracer.

Loader plugins do allow this kind of behavior, but the contract there is that they should be coded to be runnable in a non-browser environment, and are external actors on the modules. This sort of API that is used internal to a module makes that very difficult since it is more likely those module bodies will use a specific JS env APIs, like document or window, and so those modules cannot be made to generically run in tooling.

Looking towards ES6, I do not see how an async export will be compatible with how it is constructed so far. The imports are mutable slots to allow execution of modules while having a reference to the exports of another module to allow cycles to complete. This implies to me that a module body will execute before the imports are fully formed -- the module bodies are not held for exports to complete, as it would in an async export sort of model.

I will post a pointer on the amd-implement list to this issue so others will have a chance to see and consider it. This issue list is a relatively new thing so I want to be sure others comment.

— Reply to this email directly or view it on GitHub.

jakobo commented 10 years ago

I agree this is a bad pattern to propagate, but for a different reason. Asynchronous module exports would introduce a pattern that encourages blocking at the per-module level. In an application, this is a conscious choice, but with the proliferation of AMD modules on sites like bower.io, it would be too easy to unknowingly consume a library that introduces one of these blocking dependencies and destroy a site's performance.

I'd much rather see a synchronous definition that offers asynchronous methods via Promises or some other async control.

unscriptable commented 10 years ago

Very interesting observation, @Jakobo. It seems this problem (hard to find delay due to async module export) can still happen in AMD plugins and is endemic to any ES6 dynamic modules that may experience a delay in any of their loader hooks (except perhaps fetch which typically does something observable in the browser's debugging tools).

Seems that debug tooling (browser debuggers) will need to provide insight into this problem in ES6.

Note that this is not a problem when using ES6 declarative modules since the hooks aren't used.

unscriptable commented 10 years ago

Hey @evanvosberg,

I def feel your frustration. At one point, curl.js allowed async factory functions. We removed that capability, and some people complained. However, we all found different and better ways to design our code rather than rely on async factory functions as a crutch.

Sorry, but I think we should close this issue.

Any further thoughts, folks?

-- John

jrburke commented 10 years ago

@unscriptable seems general agreement so far that it should be closed.