How to solve this basic ES6-module circular dependency problem?
Bradley Meck
bradley.meck at gmail.com
Wed Aug 10 19:47:42 UTC 2016
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-dependency-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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160810/3b115b22/attachment-0001.html>
More information about the es-discuss
mailing list