Fwd: How to solve this basic ES6-module circular dependency problem?
Bradley Meck
bradley.meck at gmail.com
Wed Aug 10 20:06:51 UTC 2016
---------- Forwarded message ----------
From: Bradley Meck <bradley.meck at gmail.com>
Date: Wed, Aug 10, 2016 at 3:05 PM
Subject: Re: How to solve this basic ES6-module circular dependency problem?
To: /#!/JoePea <joe at trusktr.io>
> Bradley, true, but C is also child of A, so it can also make sense to
evaluate C before A. They are children of each other. In that case, what is
the correct order of evaluation?
The graph is traversed in order of declaration in source text, depth first.
https://github.com/trusktr/meteor/blob/issue-7621/meteor-app/client/main.
js#L23 will be evaluated only after all of https://github.com/trusktr/
meteor/blob/issue-7621/meteor-app/client/main.js#L22 has been evaluated.
* A will never load via before C, because C loads A first. (confusing
wording since we have circular stuff here)
* A attempting to load C when C has already begun evaluating results in the
short circuit at 15.2.1.16.5.4.
On Wed, Aug 10, 2016 at 2:58 PM, /#!/JoePea <joe at trusktr.io> wrote:
> Aha, when using my setup functions, rollup.js tries to evaluate C first,
> in which case the *hoisted* functions are available, but A is not declared
> yet. I don't think that this would happen in real ES6 modules (no hoisting
> like that).
>
> */#!/*JoePea
>
> On Wed, Aug 10, 2016 at 12:55 PM, /#!/JoePea <joe at trusktr.io> wrote:
>
>> Isiah, here's the [rollup.js result](http://goo.gl/jl1B8H) using my
>> setup functions technique. When I paste the result in my console it
>> complains that A is undefined inside the `setUpA` function, which seems
>> odd. Here's the [result of my original code](http://goo.gl/cbjVOi)
>> (similar to your example), and as you can see it will evaluate B first in
>> which case C will be undefined and throw an error.
>>
>> Bradley, true, but C is also child of A, so it can also make sense to
>> evaluate C before A. They are children of each other. In that case, what is
>> the correct order of evaluation?
>>
>> */#!/*JoePea
>>
>> On Wed, Aug 10, 2016 at 12:47 PM, Bradley Meck <bradley.meck at gmail.com>
>> wrote:
>>
>>> Please note that in https://tc39.github.io/ecma262/#sec-moduleevaluation
>>> , Modules evaluate their children prior to evaluating
>>> themselves (15.2.1.16.5.6.c) , C should never be evaluate before A or B
>>> in this dep graph.
>>>
>>> On Wed, Aug 10, 2016 at 2:41 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>
>>>> Oh! And although I think that my `setUpA` and `setUpB` technique should
>>>> work due to the fact that Webpack and Meteor load the modules in the exact
>>>> same order where the C module is executed last, this may in fact fail in
>>>> some other ES6 environment that happens to execute the C module first, in
>>>> which case `setUpA` and `setUpB` will be undefined when C is evaluated.
>>>>
>>>> So, I don't know if my solution is good. I am wondering if there's
>>>> something in the spec that guarantees that the C module evaluates last?
>>>>
>>>> */#!/*JoePea
>>>>
>>>> On Wed, Aug 10, 2016 at 12:38 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>>
>>>>> Isaiah, also note that
>>>>>
>>>>> ```js
>>>>> export default class Foo {}
>>>>> ```
>>>>>
>>>>> does not create a live binding that can be modified at a later point
>>>>> in time, which is the feature that my `setUpA` and `setUpB` functions are
>>>>> theoretically relying on (and which I believe the Meteor and Webpack
>>>>> environments don't handle properly if I understand live bindings correctly).
>>>>>
>>>>> */#!/*JoePea
>>>>>
>>>>> On Wed, Aug 10, 2016 at 12:31 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>>>
>>>>>> In your module B, `class B` should `extends C` instead of A, so both
>>>>>> classes `A` and `B` extend from `C`.
>>>>>>
>>>>>> I made a reproduction that you can run (assuming you have Meteor
>>>>>> installed). See the following with instructions:
>>>>>> https://github.com/meteor/meteor/issues/7621#issuecomment-238923360
>>>>>>
>>>>>> But, anyways, the example you just gave is almost identical to my
>>>>>> [original example](https://esdiscuss.org
>>>>>> /topic/how-to-solve-this-basic-es6-module-circular-dependenc
>>>>>> y-problem#content-0) (except for my `B` class extends from the `C`
>>>>>> class).
>>>>>>
>>>>>> I can make a reproduction of that too if you want, but what happens
>>>>>> is that the environment will try to execute module A before executing
>>>>>> module C, at which point `C` is `undefined` inside of module `A`.
>>>>>> Basically, the result would be the same as writing:
>>>>>>
>>>>>> ```js
>>>>>> class A extends undefined {} // throws an Error
>>>>>> ```
>>>>>>
>>>>>> I noticed that both Meteor and Webpack will try and execute modules A
>>>>>> and B *first*, then finally C, so I thought I could export the setup
>>>>>> functions andrun them after defining the C class so that even if modules A
>>>>>> and B run first the actual class definitions would run after the C class
>>>>>> definition. You can see that I'm trying to take advantage of "live
>>>>>> bindings" in order for this to work (but it didn't, hence I have to pass C
>>>>>> into the setup functions).
>>>>>>
>>>>>> I have a feeling that both Meteor's and Webpack's implementations
>>>>>> aren't up-to-spec as far as live bindings with circular dependencies, but I
>>>>>> could be wrong.
>>>>>>
>>>>>> > You shouldn't need a `setUpA` export, especially called by one of
>>>>>> its dependencies. Just declare and initialize that crap when it's being
>>>>>> declared.
>>>>>>
>>>>>> That's what I thought at first too, but it's not the case, and I'm
>>>>>> trying to find a solution.
>>>>>>
>>>>>> */#!/*JoePea
>>>>>>
>>>>>> On Wed, Aug 10, 2016 at 2:41 AM, Isiah Meadows <
>>>>>> isiahmeadows at gmail.com> wrote:
>>>>>>
>>>>>>> First of all, I'll point out that even if it's an internal API, you
>>>>>>> should just initialize them immediately. You already have an otherwise
>>>>>>> fully initialized C, so you should just add them whenever it comes. You
>>>>>>> shouldn't need a `setUpA` export, especially called by one of its
>>>>>>> dependencies. Just declare and initialize that crap when it's being
>>>>>>> declared.
>>>>>>>
>>>>>>> ```js
>>>>>>> /* index.js */
>>>>>>> import A from './app/A'
>>>>>>> console.log('Entrypoint', A)
>>>>>>> ```
>>>>>>>
>>>>>>> ```js
>>>>>>> /* app/A.js */
>>>>>>> import C from './C'
>>>>>>>
>>>>>>> export default class A eclxtends C {
>>>>>>> // ...
>>>>>>> }
>>>>>>>
>>>>>>> // set up A here
>>>>>>> console.log('Module A')
>>>>>>> ```
>>>>>>>
>>>>>>> ```js
>>>>>>> /* app/B.js */
>>>>>>> import C from './C'
>>>>>>>
>>>>>>> export default class B extends A {
>>>>>>> // ...
>>>>>>> }
>>>>>>>
>>>>>>> // set up B here
>>>>>>> console.log('Module B')
>>>>>>> ```
>>>>>>>
>>>>>>> ```js
>>>>>>> /* app/C.js */
>>>>>>> import A from './A'
>>>>>>> import B from './B'
>>>>>>>
>>>>>>> export default class C {
>>>>>>> constructor() {
>>>>>>> // this may run later, after all three modules are
>>>>>>> evaluated, or
>>>>>>> // possibly never.
>>>>>>> console.log(A)
>>>>>>> console.log(B)
>>>>>>> }
>>>>>>> }
>>>>>>>
>>>>>>> // set up C
>>>>>>> console.log('Module C')
>>>>>>> ```
>>>>>>>
>>>>>>> What's your full output, anyways? That would help me best explain
>>>>>>> what's going on, though.
>>>>>>>
>>>>>>> On Wed, Aug 10, 2016, 02:47 /#!/JoePea <joe at trusktr.io> wrote:
>>>>>>>
>>>>>>>> When I try this same code with Webpack, I get the *exact same
>>>>>>>> results*: the `console.log` statements in the exact same order, where the
>>>>>>>> last output shows that `A` in the entry point is `undefined`).
>>>>>>>>
>>>>>>>> Am I misunderstanding something about live bindings? Is there some
>>>>>>>> guaranteed order in which these modules should be evaluated?
>>>>>>>>
>>>>>>>> The reason why I'm after a solution for the circular dependency is
>>>>>>>> because in my real-world case I need to use `instanceof A` and `intanceof
>>>>>>>> B` within the `C` superclass defined in module C. This is a case of the
>>>>>>>> Fragile Base Class Problem where a class should usually *not* have
>>>>>>>> knowledge of it's subclasses, but the base class in my case is intended to
>>>>>>>> be internal only, not a part of the public API that end users will extend
>>>>>>>> from.
>>>>>>>>
>>>>>>>> */#!/*JoePea
>>>>>>>>
>>>>>>>> On Tue, Aug 9, 2016 at 11:12 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>>>>>>
>>>>>>>>> I can get the whole thing to work if I pass the C dependency into
>>>>>>>>> the `setUpA` and `setUpB` functions as follows, but oddly `A` is
>>>>>>>>> `undefined` in the Entrypoint module at the `console.log` statement, which
>>>>>>>>> makes it seem to me like live bindings aren't working the I was expecting.
>>>>>>>>>
>>>>>>>>> ```js
>>>>>>>>> // --- Entrypoint
>>>>>>>>>
>>>>>>>>> import A from './app/A'
>>>>>>>>> console.log('Entrypoint', A) // HERE, output: "Entrypoint
>>>>>>>>> undefined"
>>>>>>>>> ```
>>>>>>>>>
>>>>>>>>> ```js
>>>>>>>>> // --- Module A
>>>>>>>>>
>>>>>>>>> import C from './C'
>>>>>>>>>
>>>>>>>>> let A
>>>>>>>>>
>>>>>>>>> export
>>>>>>>>> function setUpA(C) {
>>>>>>>>>
>>>>>>>>> console.log('setUpA')
>>>>>>>>> console.log(C)
>>>>>>>>>
>>>>>>>>> A = class A extends C {
>>>>>>>>> // ...
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>> console.log('Module A', C, setUpA)
>>>>>>>>>
>>>>>>>>> export {A as default}
>>>>>>>>> ```
>>>>>>>>>
>>>>>>>>> ```js
>>>>>>>>> // --- Module B
>>>>>>>>>
>>>>>>>>> import C from './C'
>>>>>>>>>
>>>>>>>>> let B
>>>>>>>>>
>>>>>>>>> export
>>>>>>>>> function setUpB(C) {
>>>>>>>>>
>>>>>>>>> console.log('setUpB', C)
>>>>>>>>>
>>>>>>>>> B = class B extends C {
>>>>>>>>> // ...
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>> console.log('Module B', C, setUpB)
>>>>>>>>>
>>>>>>>>> export {B as default}
>>>>>>>>> ```
>>>>>>>>>
>>>>>>>>> ```js
>>>>>>>>> // --- Module C
>>>>>>>>>
>>>>>>>>> import A, {setUpA} from './A'
>>>>>>>>> import B, {setUpB} from './B'
>>>>>>>>>
>>>>>>>>> let C = class C {
>>>>>>>>> constructor() {
>>>>>>>>> // this may run later, after all three modules are
>>>>>>>>> evaluated, or
>>>>>>>>> // possibly never.
>>>>>>>>> console.log(A)
>>>>>>>>> console.log(B)
>>>>>>>>> }
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>> setUpA(C)
>>>>>>>>> console.log('Module C', A)
>>>>>>>>>
>>>>>>>>> setUpB(C)
>>>>>>>>> console.log('Module C', B)
>>>>>>>>>
>>>>>>>>> export {C as default}
>>>>>>>>> ```
>>>>>>>>>
>>>>>>>>> */#!/*JoePea
>>>>>>>>>
>>>>>>>>> On Tue, Aug 9, 2016 at 9:59 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>>>>>>>
>>>>>>>>>> It seems that the environment I'm in (Meteor uses [reify](
>>>>>>>>>> https://github.com/benjamn/reify)) tries to evaluate A and B
>>>>>>>>>> first, so I thought I could take advantage of "live bindings" by changing
>>>>>>>>>> my modules to the following:
>>>>>>>>>>
>>>>>>>>>> ```js
>>>>>>>>>> // --- Entrypoint
>>>>>>>>>>
>>>>>>>>>> import A from './app/A'
>>>>>>>>>> console.log('Entrypoint', A)
>>>>>>>>>> ```
>>>>>>>>>>
>>>>>>>>>> ```js
>>>>>>>>>> // --- Module A
>>>>>>>>>>
>>>>>>>>>> import C from './C'
>>>>>>>>>>
>>>>>>>>>> let A
>>>>>>>>>>
>>>>>>>>>> export
>>>>>>>>>> function setUpA() {
>>>>>>>>>>
>>>>>>>>>> console.log('setUpA')
>>>>>>>>>> console.log(C)
>>>>>>>>>>
>>>>>>>>>> A = class A extends C {
>>>>>>>>>> // ...
>>>>>>>>>> }
>>>>>>>>>>
>>>>>>>>>> }
>>>>>>>>>>
>>>>>>>>>> console.log('Module A', C, setUpA)
>>>>>>>>>>
>>>>>>>>>> export {A as default}
>>>>>>>>>> ```
>>>>>>>>>>
>>>>>>>>>> ```js
>>>>>>>>>> // --- Module B
>>>>>>>>>>
>>>>>>>>>> import C from './C'
>>>>>>>>>>
>>>>>>>>>> let B
>>>>>>>>>>
>>>>>>>>>> export
>>>>>>>>>> function setUpB() {
>>>>>>>>>>
>>>>>>>>>> console.log('setUpB', C)
>>>>>>>>>>
>>>>>>>>>> B = class B extends C {
>>>>>>>>>> // ...
>>>>>>>>>> }
>>>>>>>>>>
>>>>>>>>>> }
>>>>>>>>>>
>>>>>>>>>> console.log('Module B', C, setUpB)
>>>>>>>>>>
>>>>>>>>>> export {B as default}
>>>>>>>>>> ```
>>>>>>>>>>
>>>>>>>>>> ```js
>>>>>>>>>> // --- Module C
>>>>>>>>>>
>>>>>>>>>> import A, {setUpA} from './A'
>>>>>>>>>> import B, {setUpB} from './B'
>>>>>>>>>>
>>>>>>>>>> let C = class C {
>>>>>>>>>> constructor() {
>>>>>>>>>> // this may run later, after all three modules are
>>>>>>>>>> evaluated, or
>>>>>>>>>> // possibly never.
>>>>>>>>>> console.log(A)
>>>>>>>>>> console.log(B)
>>>>>>>>>> }
>>>>>>>>>> }
>>>>>>>>>>
>>>>>>>>>> setUpA()
>>>>>>>>>> console.log('Module C', A)
>>>>>>>>>>
>>>>>>>>>> setUpB()
>>>>>>>>>> console.log('Module C', B)
>>>>>>>>>>
>>>>>>>>>> export {C as default}
>>>>>>>>>> ```
>>>>>>>>>>
>>>>>>>>>> As you can see, modules A and B simply export the code that
>>>>>>>>>> should be evaluated (note the live bindings). Then finally, the C module is
>>>>>>>>>> evaluated last. At the end of the C module, you see that it calls `setUpA`
>>>>>>>>>> and `setUpB`. When it fires `setUpA`, an error is thrown on the second
>>>>>>>>>> `console.log` that `C` is undefined (or, specifically, `C.default` is
>>>>>>>>>> `undefined` because the ES6 modules are compiled into CommonJS form).
>>>>>>>>>>
>>>>>>>>>> I thought that if `C` was a live binding, then it should be ready
>>>>>>>>>> by the time the `setUpA` function is called. Should this in fact be the
>>>>>>>>>> case?
>>>>>>>>>>
>>>>>>>>>> */#!/*JoePea
>>>>>>>>>>
>>>>>>>>>> On Tue, Aug 9, 2016 at 5:36 PM, John Lenz <concavelenz at gmail.com>
>>>>>>>>>> wrote:
>>>>>>>>>>
>>>>>>>>>>> Without a way to load "later" (aka "soft") dependencies, ES6
>>>>>>>>>>> module will continue to be more or less broken for circular dependencies.
>>>>>>>>>>>
>>>>>>>>>>> On Tue, Aug 9, 2016 at 4:11 PM, Tab Atkins Jr. <
>>>>>>>>>>> jackalmage at gmail.com> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> On Tue, Aug 9, 2016 at 4:00 PM, /#!/JoePea <joe at trusktr.io>
>>>>>>>>>>>> wrote:
>>>>>>>>>>>> > True, and so that's why I'm wondering if the module system
>>>>>>>>>>>> can see that it
>>>>>>>>>>>> > can satisfy all module requirements if it simply evaluates
>>>>>>>>>>>> module C first,
>>>>>>>>>>>> > followed by A or B in any order. It is easy for us humans to
>>>>>>>>>>>> see that. It
>>>>>>>>>>>> > would be nice for the module system to see that as well (I'm
>>>>>>>>>>>> not sure if
>>>>>>>>>>>> > that is spec'd or not).
>>>>>>>>>>>>
>>>>>>>>>>>> That knowledge requires, at minimum, evaluating the rest of each
>>>>>>>>>>>> module, beyond what is expressed in the `import` statements.
>>>>>>>>>>>> That's
>>>>>>>>>>>> assuming there's no dynamic trickery going on that would
>>>>>>>>>>>> invalidate
>>>>>>>>>>>> whatever assumptions it can draw from surface-level analysis.
>>>>>>>>>>>>
>>>>>>>>>>>> Because of this, only the `import` statements are declaratively
>>>>>>>>>>>> available to the module system to work with. Based on that, it
>>>>>>>>>>>> definitely can't make any ordering assumptions; all it knows is
>>>>>>>>>>>> that A
>>>>>>>>>>>> imports C, B imports C, and C imports both A and B, making a
>>>>>>>>>>>> circular
>>>>>>>>>>>> import.
>>>>>>>>>>>>
>>>>>>>>>>>> ~TJ
>>>>>>>>>>>> _______________________________________________
>>>>>>>>>>>> es-discuss mailing list
>>>>>>>>>>>> es-discuss at mozilla.org
>>>>>>>>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>
>>>>>>>> _______________________________________________
>>>>>>>> es-discuss mailing list
>>>>>>>> es-discuss at mozilla.org
>>>>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>>>>
>>>>>>>
>>>>>>
>>>>>
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>>
>>>
>>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160810/3acd697d/attachment-0001.html>
More information about the es-discuss
mailing list