tc39 / proposal-built-in-modules

BSD 2-Clause "Simplified" License
891 stars 25 forks source link

Why 'modules' instead of built-in? #50

Open riggs opened 5 years ago

riggs commented 5 years ago

The current proposal declares access to the standard library will be exposed via imports, but provides little reasoning for the decision. Given that there are multiple current built-ins that are not language features, but merely components of an effective standard library (e.g. Math, Date, JSON), exposing additional components via import is a significant deviation from current developer expectations.

The only clearly presented argument I can glean from the proposal is that imports may help reduce global namespace collisions. While I can understand this is a potential problem, I question how much of a problem it is in practice. Adding a single new global parent, such as std, for the new library features would also help avoid namespace collisions.

The majority of the proposal deals with handling the vastly increased complexity resulting from the design decision to extend the import mechanism. While the proposed import behavior is modeled on python's behavior, the operational environment of python, from which python's imports derive, is drastically different than that of the browser. While JS isn't used exclusively in the browser, it was designed for it, adapting existing JS mechanisms to other environments is work that has already been completed successfully.

To me, this proposal feels like shoe-horning a python-like standard library despite the design and operational constraints already imposed by the existing language ecosystem resulting in significant increased complexity both for the engine and for developers.

ericblade commented 5 years ago

A short version, is that whenever any new global object or function is introduced into the system, you risk breaking something, somewhere. Potentially, the environment stomps on the code, or the code stomps on the environment. Adding anything to the global namespace, as has been done for many years now, risks breaking some piece of code or other.

We should not continue to use, add on to, and promote poorly designed things, just because that's what we've always done before.

With an explicit import for each piece you use, if you want to extend that to your own version of something, or you want to use a completely different implementation of something, there's no monkeypatching involved. You just change the import. Done. Monkeypatching -also- risks breaking everything. So, we remove global namespace collisions, and solve a large quantity of monkey patching potential problems, by using a new language feature.

Continuing to do what we've always done, is simply shoe-horning the old ways into a system that can do better.

(note: "we" means all of the javascript developers, i'm not trying to speak on behalf of all the people at this repo specifically, and I'm just a watcher here for the moment)

ljharb commented 5 years ago

Due to import maps (and the mutability/polyfill requirements), this is already an identical risk with this current proposal.

The only capability/improvement I’m aware of that this proposal adds (contrasted with the abilities it removes: sync usage in Scripts, for one), is the ability (via import maps or similar) to provide different builtins to different modules.

kaizhu256 commented 5 years ago

as a former python-programmer, i agree with @riggs view, that that javascript is not python and occupies completely different problem-space (ux-workflow vs general-purpose programming).

logarithmically adding builtins to global-namespace may not be ideal, but the reality is its far more practical than using es6's import-mechanism. i consider javascript's existing [battle-tested] tc39/w3c/whatwg api's as mostly mature, such that the pace for adding new global api's shouldn't be that worrisome in forseeable future.

msaboff commented 5 years ago

We have been adding to the global namespace for 20+ years. There have been notable collisions that have been problematic and broke things in unexpected ways. Certainly having a global namespace is an anti pattern for most developers. When a developer adds their own objects to the global namespace, they run the risk of collisions as other objects are added to the global namespace by standards or frameworks.

Having such a large namespace also has memory and startup implications. Certainly attributes and prototypes can be lazily initialized as they are used, but in general a large and mostly unused global namespace is detrimental to both memory and startup time.

As @ericblade points out, probably the most important reason for the current proposal is to provide greater assurances for developers as to what they are getting. Right now, some package can monkey patch a standard method for a built-in, giving all modules that monkey patched method. To some developers, this is a positive things, but for most this would provide lots of confusion. Consider the following trivial example. A framework developer depends on the standard behavior of Array.prototype.join() with no arguments. Another framework or the main program monkey patches join() to use a comma instead of space for the default joined separator. Discovering this odd breaking dependency and working around it is frustrating and time consuming. The current proposal prevents this kind of problem for users of standard library modules. Certainly importing code could extend or wrap an imported module, but the impacts of such extension are limited to that importing code and if it is another module that is importing, the code that imports the extending module. The proposed pattern is that the extending module would be distinguishable from the standard one, supporting the principle of least surprise.

kaizhu256 commented 5 years ago

Right now, some package can monkey patch a standard method for a built-in, giving all modules that monkey patched method. ... Discovering this odd breaking dependency and working around it is frustrating and time consuming.

that certainly is a PITA, but is it more so than working with es-modules? there are people like me who would say no -- monkey-patching occasional global-namespace collisions (ultimately to achieve some UX-workflow objective) is actually easier to deal with than working with es-module magic.

ljharb commented 5 years ago

@msaboff the problem is that that capability is critical to this proposal proceeding - iow, mutability of builtins is required, whether from a global or from a module.

The memory considerations are something else, perhaps, but I’m confused why the majority of globals can’t be lazily loaded?

riggs commented 5 years ago

@msaboff Are there advantages to importing new features vs, say, putting them under a global 'stdlib' namespace? Jquery & lodash have long done a form of precisely this.

msaboff commented 5 years ago

@ljharb, import maps are the path to mutability of standard library contents. From what we have determined, adding mutability to this proposal is harder than what import maps provides. The fundamental issue is that mutability is best done from the host and not the language at module loading time.

We are planning that the module resolution and loading of JavaScript standard libraries integrates seamlessly with import maps.

msaboff commented 5 years ago

@msaboff Are there advantages to importing new features vs, say, putting them under a global 'stdlib' namespace? Jquery & lodash have long done a form of precisely this.

Off the top of my head, here are a couple of advantages to importing over adding to the global namespace:

1) Importing on a feature by feature basis allows for clear delineation of what is loaded. This impacts both namespace as well reducing execution resources. 2) It is quite possible that multiple application modules import "competing" modules where one of the imported modules is a standard library. There could be namespace collisions, or the access to a feature's objects becomes nested deeply, e.g. window.stdlib.acme.widget vs window.stdlib.builtin.widget. With each module importing the flavor of widget it wants, there won't be namespace collisions, global namespace pollution or confusion as to which "widget" is which.

kmiller68 commented 5 years ago

@riggs Using a global 'stdlib' namespace has essentially the same set of problems that putting things in the global namespace has. As soon as new features start appearing in the 'stdlib' namespace then people will start adding features they consider missing to it. Once that happens we are in the same namespace collision boat again.

I expect that JQuery and lodash don't have the same namespace collision problem that browsers have because a website has to update themselves to the newest version of JQuery and/or lodash. Thus, any breakage happen due to a change on the part of the website's author not JQuery/lodash. I suppose a site could load a framework from some 3rd party source but then they would be at the whims of whatever that 3rd party decides to deliver so I expect very few sites actually do this.

ljharb commented 5 years ago

@msaboff i don't mind if hosts decide how to implement it, but i think it's critical that hosts be mandated to implement some mechanism in order to be 262-compliant, if 262 is going to ship functionality that's only available via built-in modules.

riggs commented 5 years ago

Lazily loading is already commonplace, so I'm doubtful of any assertions of performance improvement. The code for the standard library modules will be as accessible to the engine either way.

Based on discussions here, it seems the combined design goals of deeply freezing imported modules while still providing a mechanism for polyfilling is the largest driver for imports. Supporting both of these features would be impossible with a built-in object. If not for the desire to enable polyfillls, a simple frozen object would be a much cleaner design.

kaizhu256 commented 5 years ago

With each module importing the flavor of widget it wants, there won't be namespace collisions, global namespace pollution or confusion as to which "widget" is which

again dealing with global-namespace collisions is generally a lesser evil than dealing with module-loading magic in [high code-churn] frontend-development. its fairly straightforward to rename global objects with longer unique-names using grep-and-replace when collisions occur. otoh, there's nothing straightforward on how to load different "flavors" of a es-module/widget, and the tech-debt required to do so.

the c++ mindset of overloading short variable names inside namespaces/classes is a terrible design-pattern in ux-workflow programming.