-
Notifications
You must be signed in to change notification settings - Fork 44
"Transparent" Interop is a confusing term #138
Comments
+1 to deprecation Do you have a suggestion for alternative? Mixed Mode Interoperability? |
How about just "interop"? In the context of the module discussion that seems sufficient. Potentially with "backward interop" and "forward interop" to describe the two directions? |
I don’t really understand where the confusion comes here - it seems an
incredibly well defined term to me:
Given a parent module written in a module format, the ability to load a
child module of a different module format from the parent using the natural
native module syntax of the parent module.
The term interior covers the fact we are bridging module systems.
The term transparent covers that we are using the existing module format
machinery.
What are the confusions we’re seeing here with this definition? Can we
clear them up somehow?
…On Mon, 25 Jun 2018 at 19:35, Myles Borins ***@***.***> wrote:
+1 to deprecation
Do you have a suggestion for alternative?
Mixed Mode Interoperability?
Overloaded Import?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#138 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAkiytk1yNXD3DHbYR6ptlIqQC_GCwZEks5uAR9ZgaJpZM4U2ira>
.
|
I interpret “transparent” to mean, “as a user, I don’t need to know the module type.” I agree it can be confusing, but I think maybe it really is this simple? The core of it is that I can import a package without needing to know whether that package is ESM or CommonJS. The rest of transparent interop flows from that: I need to be able to export from a package such that this “blind” import works, etc. Ditto for other ways of importing and exporting modules, such as importing or running single files or strings. One especially confusing area is the “drop the file extension” part of the This assumes that interoperability is limited to importing and exporting, and perhaps there are other forms of interop that I’m not considering, in which case maybe those other parts should get another term? And I’m sure there are other things that people have lumped under this rubric that perhaps should have other terms, but I think “transparent interop” is definable. |
I don't think we can have a single term. The problem I think is that it covers too many things, we need to classify different aspects of interoperability. Per my thoughts on how to classify things:
Part of the problem also is the usage of
We often treat the idea of isomorphism as all or nothing when using the word "transparent" which makes it hard to discuss transition paths and compatibility for things that only wish to have a path, even if not universal:
In addition we are also using the term to discuss specific, often configurable, implementations of loading steps:
|
@GeoffreyBooth “as a user, I don’t need to know the module type" is why .mjs is transparent (because only the author needs to know that, not the consumer). I think we should differentiate between, to name a few:
All of these are facets of "transparent", but not its entirety. |
Some people think transparent interop is obvious/well-defined and others interpret it as some combination of independent attributes. I believe the simplicity of the term is unhelpful and is preventing us from communicating clearly in discussions. Maybe if we define the terms precisely we can better debate the problem space. I've made an attempt at defining these terms loosely based on @bmeck and @ljharb 's breakdown using @SMotaal 's latest glossary document as a place to hold the definitions. I would invite people to comment/edit the document so we can iterate on the terms one place, as opposed to building ever-longer issues. To be clear: my opinion is that the term transparent interop has now become so over-used for different things that we should (a) deprecate/suspend using the term until we can agree that it has one written definition, and (b) encourage the usage of more precise terms where possible. I'm not attached to the new terms in the doc or their definitions so feel free to refactor vigorously until they make sense. |
@robpalme that is a good direction to take this I think. I personally would like it to be split up more, but think adding terms separately might be enough. |
@robpalme Thanks. I think it's interesting that we are converging around the definitions of "require interop" and "import interop". In your definitions, you specifically call out some challenges with each interop direction:
|
@zenparsing thanks for the comments.
|
@robpalme I'd add the Promise wrapping variant because I don't think that the ability for APIs interoperate deals with isomorphism of the APIs. |
@bmeck Sure. We can go for completeness if you think it's important. But is there anyone that would ever want a variant of |
@robpalme people shipping libraries to versions of Node that don't support ESM and want to create an API that works the same as in a version of Node that supports ESM. They could be compiling ESM down to Promises using something like babel, or they could be directly writing CJS that is isomorphic to people on versions of Node supporting ESM. Basically, anyone who wishes to continue support consumers using |
@bmeck I agree that "require interop" does not necessitate isomophism of the module's API. On the other hand, I'm not sure we should call Promise-returning Let's assume for the sake of argument that we have a version of "require interop" that returns promises for module namespace objects. If I have a module In such a case, package authors and consumers are forced to agree on the module "mode", which is something I think we are trying to avoid with interop. |
@zenparsing in my description above this is exactly why I had module.exports = async () => {
await null;
// ...
};
module.exports = module.exports(); The above example does produce a situation with timing isomorphism with an ESM if it were to be returned from I agree in your example that asynchrony is viral in nature, but that is unrelated to interoperability. Interoperability on its own is related to the ability to use something from a different thing (in this case ESM from |
I agree, but it appears to me that if I consume a promise-returning From that point of view, If not, then I would say that any essential asynchronicity (like top-level await) breaks "require interop", regardless of whether node or a custom loader is providing that interop support. |
@zenparsing in my example above it works perfectly well to put that in a CJS file today, what makes the distinction that it is not CJS but something else? How is a CJS file that exports a |
@zenparsing const promise = require('some-async-thing');
module.exports = { foo: null };
promise.then(x => { module.exports.foo = x; });
// or
let cache;
module.exports = function getFoo() { return cache; }
promise.then(x => { cache = x; }); It's certainly not idiomatic, but there's all sorts of cases where requiring a promise need not force the export to be a promise in current CJS. |
@ljharb I'm not sure I would call that "live bindings". I'd call that zalgo! 😵 |
I don't think we can give the list of names on the export to form that module namespace synchronously, so I don't think we could preserve that exact code since it doesn't wrap the whole export in a Promise. |
@zenparsing it's identically as z̲̗̼͙̥͚͛͑̏a̦̟̳͋̄̅ͬ̌͒͟ļ̟̉͌ͪ͌̃̚g͔͇̯̜ͬ̒́o̢̹ͧͥͪͬ as ESM live bindings :-p |
I was totally trying to figure out how to put that graphic in there and settled for dizzy face. You win! |
@bmeck |
@demurgos the consumer could still get access to As a related topic, if the library were to move to ESM and the consumer were to remain CJS/use |
See #139 for a more detailed explanation of my issue with this solution. The goal of the pattern you provided is to enable the lib to move from CJS to ESM without breaking a CJS consumer (as you stated in your second paragraph). But if the consumer switches to ESM before the lib, then the lib migration becomes a breaking change in this case. It means that the library needs to both update its API and switch to ESM in a single step to avoid this kind of situation. |
Rather than pursuing that line of argument, I think I'd just like to say that module.exports = (async () {
const foo = await require('foo');
})(); (Also, if I am using Babel to transpile my ESM to CJS, is Babel supposed to generate the above?) |
In the spirit of permitting CJS to be written for today's Node (pre-ESM) that can cope with importing future ESM module graphs containing Top-Level Await, I find @bmeck 's arguments in favour of This can be achieved today by creating a CJS module that just exports a promise containing an object that meets the constraints of ESM namespace (using restricted identifiers, not callable, etc). Then in future Node with ESM, It may not be to everyone's tastes, but it does solve a problem. In terms of the tradeoffs, and how CJS-ey it feels to use, I can understand @zenparsing 's view that this is not the interop many people would want, i.e. users would prefer to forfeit compatibility with future module graphs that contain Top-Level Await because (maybe) that will be a niche feature. So I think at least we can give these two types of require interop names:
What do you think? |
I think that promise-wrapping on the CJS side is an anti-pattern because it complicates the migration of consumers to ESM. If you can use |
@demurgos you cannot use
Can you clarify what is surprising / what is wrong with the API? Also people may ship their own form of dual builds that ship a synchronous CJS form and ESM form which doesn't seem to be taken into account; the recommendation for that migration is about when they are seeking to transition entirely to ESM, not around multi-goal support. |
@bmeck
My assumption is that if you have My issue is that if the goal of
The drawbacks of the Promise-Wrapped Plain Object method require the consumer to be aware of the implementation of the lib. This defeats the agnostic consumer goal. Once this goal is out of the table, I mentioned dual-builds as an alternative in my post. |
@robpalme That summary looks reasonable, and I really want to agree with it, but I'm having a hard time. Unfortunately, I simply can't imagine a package author asking users to call "then" on their library. Nor do I understand how Babel is supposed to generate the call to "then" if the package consumer is transpiling. Does Babel convert all static imports into If there were no top-level await, then the promise-wrapping issue with "require interop" would dissipate. It may turn out that top-level await creates significant interop hazards; perhaps we should not be assuming it? |
There is not yet a TLA, so i think we should be discussing things primarily without assuming it (not that we should ignore it). |
Even without TLA, |
@bmeck Let's try to unpack some of that.
True, but
Can you expand on this a bit? For the sake of argument, let's assume that "import interop" does not support named exports.
I don't have a very clear picture of what this scenario looks like. Are we talking about a custom loader that loads non-JS files, or a custom loader that loads JS files in a different way, or both? |
Zebra striping is a term from https://github.com/whatwg/loader/blob/0093dc874b9739c8cbf96a5994de2b923778e4e5/rationale.md#dynamic-modules-do-not-have-tracked-dependencies and conversations that led up to that point. It is the concept of having multiple "colors" of the module graph while evaluating code and generating bindings. In particular the problem arises from when the list of bindings is dynamic in some fashion. You must evaluate things with dynamic shapes (such as CJS under babel's interop) prior to binding them in the graph. This is doable if the CJS subgraph is a leaf of the entire ESM graph. However, once you have ESM importing CJS which cycles with ESM you have a problem. The ESM in a cycle with CJS must evaluate prior to the CJS that does not yet have a shape (because it is still evaluating). CJS -> ESM -> CJS suffers the same issue. I have some old slides on this topic that may better illustrate. We have a few scenarios here and I'll try to describe 3 that come to mind:
This is what non-ESM only generating a
There is no zebra striping.
There is zebra striping going on here. This is not able to be achieved in a way that is possible in the current specification. Adding a late binding mechanism would allow this to be achieved at the cost of not being able to generate your binding graph prior to evaluation.
Loaders can return any supported format, so probably can output CJS, WASM, and ESM at least. They don't necessarily resolve to URLs that point to JS and they don't even necessarily return the same format as that which they resolve to (could compile ESM to CJS or vice versa for example). |
One example: If we allow resolving a module graph synchronously, we will lock ourselves out of Forcing |
Thanks for expanding on that. I agree both that we want to avoid any zebra-striping and that we want to support a wide range of async custom loading scenarios.
Thanks for the concrete example. I agree that graph loading should be async to support all of these use cases. |
AFAIK It's recommended because in chrome v8 limits synchronous wasm evaluation to something like 500kb to prevent thread hangs from large wasm blobs. It's less of a problem in node, where a thread "hang" on a dependent resource is sometimes the desirable outcome (or at least always is for a |
its recommended in every platform that supports wasm. the format is designed to be streamed. its simply good design. |
Streaming the parse result only matters if you process the stream as it comes in, otherwise all it does is affect the API required to do the same thing (and prevent a thread lock). No esm consumer is going to do something on a partially parsed wasm file, or anything, for that matter, while imports are being resolved. Afaik, the only observable difference would be weather the just thread is locked or not when you use dynamic import. For import statements, weather the resolver you use streams or not is irrelevant; the resulting API is the same. |
That's a weird assumption. |
And you could probably use the synchronous API for a statement based request and the async one for a dynamic import if need be. |
In an effort to progress the original issue, I have made a PR to define terms for interop across public package boundaries (Agnostic Package Consumers) vs the opposite (Agnostic Module Consumers). Please use that PR as a place for discussing these specific terms. |
Can this be closed or merged into another issue? |
To my knowledge we still don't have a clear definition for "transparent" but are still using the term. |
this term is not being as heavily used these days and i think confusion has been alleviated. |
We should try and find a better set of terms as this usage of the word "transparent" is proving to be different across multiple discussions / viewpoints. I'd suggest we split up different points and avoid using the word "transparent" when talking about things. How do other people feel about deprecating using the term "transparent" when talking about interoperability?
The text was updated successfully, but these errors were encountered: