How to solve this basic ES6-module circular dependency problem?
/#!/JoePea
joe at trusktr.io
Wed Aug 10 20:21:49 UTC 2016
Bradley, thanks! Based on what you've pointed out, if the entrypoint
imports only B from module B, then that should cause the import lookup to
go to module C, which sees the import for A and causes the lookup to go to
module A, and finally depth first executes A, then C, then finally B. This
is in fact what I'm [seeing in rollup.js](http://goo.gl/SQetDd).
Users of my API (names are more meaningful in reality, but I have three
modules defining classes in exactly the same way) will only ever import A
or B directly, but never C, so I should be able to use that fact to
formulate a solution. I'm not sure what the solution is yet though.
*/#!/*JoePea
On Wed, Aug 10, 2016 at 1:06 PM, Bradley Meck <bradley.meck at gmail.com>
wrote:
>
> ---------- 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/ecma
>>>> 262/#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
>>
>>
>
>
> _______________________________________________
> 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/b2f63cb8/attachment-0001.html>
More information about the es-discuss
mailing list