How to solve this basic ES6-module circular dependency problem?

/#!/JoePea joe at trusktr.io
Sun Oct 9 00:23:36 UTC 2016


Hello Logan, I wonder if I can borrow your expertise for just a moment:

I ran into another problem. Suppose I have the following code which is very
similar to the code I had before (using the `var` trick along with `initC`
function), but now Module C imports `someFunction` from a newly added
module.

The problem is that module B will be evaluated before both Module C and the
utilities module. When `initC` is called inside of the B module, the
`initC` function will try and call `someFunction` but there will be an
error because the utilities module was not yet evaluated, so `someFunction`
is undefined.

What would you recommend as a solution?

Here's the code:

```js
// --- Entrypoint
import A from './A'
console.log('Entrypoint', new A)
```

```js
// --- Module A

import C, {initC} from './C'

console.log('module A')
initC()

class A extends C {
    // ...
}

export {A as default}
```

```js
// --- Module B

import C, {initC} from './C'

console.log('module B')
initC()

class B extends C {
    // ...
}

export {B as default}
```

```js
// --- Module C

import A from './A'
import B from './B'
import {someFunction} from './utilities'

console.log('module C')

var C

initC()

export function initC(){
    if (C) return

    console.log('initC!!!')
    someFunction()

    C = class C {
        constructor() {
            // this may run later, after all three modules are evaluated, or
            // possibly never.
            console.log(A)
            console.log(B)
        }
    }
}

export {C as default}
```

```js
// utilities.js

var someFunction = () => {...}

export {
    someFunction,
}
```

I think a possible (though strange) solution is to change the import order
in Module C, f.e.:

```js
import A from './A'
import B from './B'
import {someFunction} from './utilities'
```

I haven't actually tested that example code, but I believe I'm having a
problem like this with Webpack+Babel here:
https://github.com/trusktr/infamous/tree/circular-dep-bug

In this case, there is a circular dependency between `src/motor/Scene` and
`src/motor/Sizeable` (which I've already planned to eliminate, but am still
curious about how I would solve it without eliminating the circular dep).
The modules import the defaults of each other, and I'm using the `var
Sizeable/initSizeable` trick. However, there's a third module, `Utility.js`
from which the `Sizeable` module needs to import something to use inside of
the `initSizeable` function.

You can experience the error by simply opening `motor-scratch.html` in your
browser, in which case you'll get an error like

```
Sizeable.js:328: Uncaught TypeError: Cannot read property '
makeLowercaseSetterAliases' of undefined
```

where `makeLowercaseSetterAliases` is akin to the `someFunction` of my
earlier example. The odd thing is that everything works fine in Meteor's
Reify environment, and I'm only getting this error when using the bundle
made by Webpack+Babel.

If I re-order the dependencies in `src/motor/Sizeable` so that they look
like this:

```js
import { makeLowercaseSetterAliases } from './Utility'
import XYZValues from './XYZValues'
import Motor from './Motor'
import Scene from './Scene'
```

then the error goes away, but I get another error:

```
node.js:129: Uncaught TypeError: Cannot read property 'prototype' of
undefined
```

I have a feeling that moving the import to the beginning solved the
problem, and that now I have another problem...

If you'd like to test changes (f.e. moving the import statement), you can
simply:

```
npm install
npm run watch
```

which will automatically compile the global.js file after making changes to
anything in `src`, and you can reload the page in your browser.

Any and all input you may have on this would be greatly appreciated!

Thanks a ton!

All the best,
- Joe


*/#!/*JoePea

On Tue, Aug 23, 2016 at 8:55 PM, Logan Smyth <loganfsmyth at gmail.com> wrote:

> > Should I open an issue there? And I think you're right about it should
> throw, because it is strange that the function can be executed before the
> module is ever evaluated (that seems like it should be impossible), and
> therefore a TDZ error would happen because the `let` line wasn't
> theoretically evaluated yet.
>
> If you'd like, go for it. To note, Babel 6 doesn't implement TDZ at all at
> the moment anyway.
>
> >  I don't see how it is possible with `var`. How is it that `var`s or
> `function`s can be hoisted *out of* the module? Is that part of spec? If
> so, then that is different hoisting from function-based hoisting of pre-ES6.
>
> They are not hoisted out of the module. It seems like you may be
> misunderstanding how modules are linked together.
>
> > Pre-ES6 "hoisting" in javascript happens on boundaries set by
> `function`s, and I thought that modules would be similar to function
> bodies, and therefore I thought that hoisting would be limited to within a
> module and did not expect hoisting to go beyond the module boundary to some
> scope that encompasses multiple modules. That to me is a different type of
> "hoisting" than what I know from pre-ES6 JavaScript's function-scope
> hoisting so it isn't necessarily the "same way it'd work with other cases
> of hoisting"; there is definitely some ES6-module-specific stuff happening
> that is a little different from pre-ES6 function-scope hoisting. (sidenote:
> I've asked the awesome Axel of 2ality to add these useful details to his
> articles.)
>
> Hoisting of var and functions behaves the same way in ES6 and in ES5 and
> you are correct there is no "beyond the module boundary". Let's clarify
> hoisting in a function.
>
> ```
> function fn() {
>     console.log(inner());
>
>     function inner(){ return "hello world"; }
> }
> fn();
> ```
> in this context, when an engine executes `fn()`, before any execution has
> happened inside the `fn`, the engine does the following
>
> 1. Create the conceptual function "scope", which exists immediately as
> soon as the function is called.
> 2. Look for all function declarations inside `fn` and create their
> variables and assign their values to point to function objects. This is
> "function hoisting".
> 3. Look for all var declarations inside `fn` and create variables with the
> value `undefined`. This is "var hoisting".
> 4. Look for all let/const declarations, and create uninitialized variables
> (these will throw when accessed)
> 5. A bunch of other stuff I'm skipping
> 6. Execute the function body itself.
>
> A very similar process happens for modules. You can think of it like steps
> 1-4 running, then before executing the module body (step 6), we recursively
> do this same process on every imported module. So by the time any module
> gets to step 6, every imported module, and every module those modules
> depend on, will have executed step 1-4, and have scopes that have been
> created, and variables that have been declared (possibly with a value, or
> possibly left uninitialized).
>
> So by the time you get to step 6, there isn't a "beyond the module
> boundary", when you access the imported variables in your module, the JS
> engine will reach across the module boundary for you, to get the current
> value of the variable in the imported module. This behavior of reaching
> across module scopes is what module syntax allows, and it is what enables
> live binding.
>
> Because of this live behavior, if you imported something that was defined
> with `let`, like your `let C;` example, it would cause a TDZ error because
> the variable was still "uninitialized", whereas if you make it a `var`, it
> will be initialized to `undefined`.
>
> > If I use the `var` method as you proposed (which is working in my Babel
> env), should I expect that method to always work in any theoretical
> 100%-implemented-to-spec ES6 environment, not just in Babel?
>
> Correct.
>
> On Tue, Aug 23, 2016 at 8:17 PM, /#!/JoePea <joe at trusktr.io> wrote:
>
>> > Damn, that's a Babel bug with the block scoping logic. That said, as in
>> my example, that needs to be `var C;` anyway, `let` would throw (in an
>> environment with working TDZ anyway). Changing it to `var` also stops the
>> duplicate printing.
>>
>> Should I open an issue there? And I think you're right about it should
>> throw, because it is strange that the function can be executed before the
>> module is ever evaluated (that seems like it should be impossible), and
>> therefore a TDZ error would happen because the `let` line wasn't
>> theoretically evaluated yet.
>>
>> I don't see how it is possible with `var`. How is it that `var`s or
>> `function`s can be hoisted *out of* the module? Is that part of spec? If
>> so, then that is different hoisting from function-based hoisting of pre-ES6.
>>
>> I was under the impression that the modules were like a function, and
>> hoisting would only happen inside the module. In that case, the `initC`
>> function could not possibly be available until the `C` module itself was
>> evaluated, so I was expecting for there to be an `undefined` error when
>> `initC` was called before the `C` module was evaluated.
>>
>> You say that
>>
>> > which means you can import and call a function declaration from any
>> module, even if that module hasn't started the `Evaluation` phase yet, the
>> same way it'd work with other cases of hoisting, where execution hasn't
>> reached the function declaration, but it is available early.
>>
>> Which makes sense based on what I see happening, but it seems strange
>> because it means that the scope of the module (as far as hoisting is
>> concerned) is not the module itself, but rather some outer scope that wraps
>> *all* the modules that import a given symbol.
>>
>> You said,
>>
>> > the same way it'd work with other cases of hoisting, where execution
>> hasn't reached the function declaration, but it is available early.
>>
>> Pre-ES6 "hoisting" in javascript happens on boundaries set by
>> `function`s, and I thought that modules would be similar to function
>> bodies, and therefore I thought that hoisting would be limited to within a
>> module and did not expect hoisting to go beyond the module boundary to some
>> scope that encompasses multiple modules. That to me is a different type of
>> "hoisting" than what I know from pre-ES6 JavaScript's function-scope
>> hoisting so it isn't necessarily the "same way it'd work with other cases
>> of hoisting"; there is definitely some ES6-module-specific stuff happening
>> that is a little different from pre-ES6 function-scope hoisting. (sidenote:
>> I've asked the awesome Axel of 2ality to add these useful details to his
>> articles.)
>>
>> If I use the `var` method as you proposed (which is working in my Babel
>> env), should I expect that method to always work in any theoretical
>> 100%-implemented-to-spec ES6 environment, not just in Babel?
>>
>> If so, then this may be one of the rare cases of "when we'd want to
>> actually use `var` instead of `let`" besides for cases when we want pre-ES6
>> hoisting which I think should be generally avoided in order to make code
>> less error-prone and easier to understand. This behavior would have been
>> nearly-impossible to know about without the knowledge gained from this
>> conversation (or from reading the spec in depth which can be difficult).
>>
>>
>>
>> */#!/*JoePea
>>
>> On Tue, Aug 16, 2016 at 10:48 AM, Logan Smyth <loganfsmyth at gmail.com>
>> wrote:
>>
>>> > Your `initC` solution is working in Meteor (Babel + Reify) and
>>> Webpack+Babel, but the `initC` logic seems to run twice, as if there are
>>> two C variables instead of one. The following code is based on yours, and
>>> the `console.log('initC!!!')` statement unexpectedly executes twice, and
>>> you'll see output like this:
>>>
>>> Damn, that's a Babel bug with the block scoping logic. That said, as in
>>> my example, that needs to be `var C;` anyway, `let` would throw (in an
>>> environment with working TDZ anyway). Changing it to `var` also stops the
>>> duplicate printing.
>>>
>>> > I'm also not sure how `initC` can be defined when it is called in the
>>> B module, which is evaluated before C and A. The evaluation order is B, C,
>>> A (depth first). Does the `initC` function get hoisted into a scope common
>>> with all three modules? That is the only way that would seem to explain it,
>>> but that seems to go against the intuition I had that each module had it's
>>> own module scope (as if it were wrapped inside a `function() {}`, and
>>> therefore I thought the `initC` function would be hoisted within the C
>>> module, and that with B being evaluated first I thought an "undefined"
>>> error would be thrown when it tried to execute `initC` (but that is not
>>> happening!). How is it that `initC` can be available to the B module before
>>> C is evaluated?
>>>
>>> There are two separate pieces to executing a module, `Instantiation`,
>>> and `Evaluation`, which are what comes into play here. When you tell a JS
>>> environment to execute a file, it will instantiate every ES6 module in
>>> dependency graph before beginning to execute _any_ of the modules. Babel
>>> does its best to simulate this behavior, though it's not perfect at it. One
>>> of the things that happens during module instantiation is that hoisted
>>> declarations are initialized, which means you can import and call a
>>> function declaration from any module, even if that module hasn't started
>>> the `Evaluation` phase yet, the same way it'd work with other cases of
>>> hoisting, where execution hasn't reached the function declaration, but it
>>> is available early.
>>>
>>> This behavior is why you can't use `let C` there, because when `B` is
>>> being evaluated, the `let C` line won't have run because the Evaluation
>>> phase of `C` hasn't started yet. You can however access the `initC`
>>> function because it is a function declaration. As long as there are no TDZ
>>> errors in what you're doing, that function can do whatever it would like,
>>> assuming it doesn't depend on other stuff that would require `Evaluation`
>>> to have finished in `C`, the same as what happens with hoisting normally.
>>> That means for instance you couldn't do
>>>
>>>     import B from './B';
>>>     var SOME_CONSTANT = "hello";
>>>
>>>     export function initC(){
>>>         return SOME_CONSTANT;
>>>     }
>>>
>>> because calling `initC` here would return `undefined` if called from
>>> inside a dependency cycle from `B`.
>>>
>>> On Sat, Aug 13, 2016 at 9:09 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>
>>>> Hi Logan,
>>>>
>>>> > The example I posted works properly with Babel's live-binding
>>>> implementation and should require less repetition. What were your thoughts
>>>> on it?
>>>>
>>>> Your `initC` solution is working in Meteor (Babel + Reify) and
>>>> Webpack+Babel, but the `initC` logic seems to run twice, as if there are
>>>> two C variables instead of one. The following code is based on yours, and
>>>> the `console.log('initC!!!')` statement unexpectedly executes twice, and
>>>> you'll see output like this:
>>>>
>>>> ```
>>>> module B
>>>> initC!!!
>>>> module C
>>>> initC!!!
>>>> module A
>>>> function A() { ... }
>>>> function B() { ... }
>>>> Entrypoint A {}
>>>> ```
>>>>
>>>> I'm also not sure how `initC` can be defined when it is called in the B
>>>> module, which is evaluated before C and A. The evaluation order is B, C, A
>>>> (depth first). Does the `initC` function get hoisted into a scope common
>>>> with all three modules? That is the only way that would seem to explain it,
>>>> but that seems to go against the intuition I had that each module had it's
>>>> own module scope (as if it were wrapped inside a `function() {}`, and
>>>> therefore I thought the `initC` function would be hoisted within the C
>>>> module, and that with B being evaluated first I thought an "undefined"
>>>> error would be thrown when it tried to execute `initC` (but that is not
>>>> happening!). How is it that `initC` can be available to the B module before
>>>> C is evaluated?
>>>>
>>>> This is the code I have:
>>>>
>>>> ```js
>>>> // --- Entrypoint
>>>> import A from './A'
>>>> console.log('Entrypoint', new A)
>>>> ```
>>>>
>>>> ```js
>>>> // --- Module A
>>>>
>>>> import C, {initC} from './C'
>>>>
>>>> console.log('module A')
>>>> initC()
>>>>
>>>> class A extends C {
>>>>     // ...
>>>> }
>>>>
>>>> export {A as default}
>>>> ```
>>>>
>>>> ```js
>>>> // --- Module B
>>>>
>>>> import C, {initC} from './C'
>>>>
>>>> console.log('module B')
>>>> initC()
>>>>
>>>> class B extends C {
>>>>     // ...
>>>> }
>>>>
>>>> export {B as default}
>>>> ```
>>>>
>>>> ```js
>>>> // --- Module C
>>>>
>>>> import A from './A'
>>>> import B from './B'
>>>>
>>>> console.log('module C')
>>>> let C
>>>>
>>>> export function initC(){
>>>>     if (C) return
>>>>
>>>>     console.log('initC!!!')
>>>>
>>>>     C = class C {
>>>>         constructor() {
>>>>             // this may run later, after all three modules are
>>>> evaluated, or
>>>>             // possibly never.
>>>>             console.log(A)
>>>>             console.log(B)
>>>>         }
>>>>     }
>>>> }
>>>>
>>>> initC()
>>>>
>>>> export {C as default}
>>>> ```
>>>>
>>>> */#!/*JoePea
>>>>
>>>> On Thu, Aug 11, 2016 at 10:26 AM, Logan Smyth <loganfsmyth at gmail.com>
>>>> wrote:
>>>>
>>>>> Keep in mind `let A = A;` is a TDZ error in any real ES6 environment.
>>>>>
>>>>> The example I posted works properly with Babel's live-binding
>>>>> implementation and should require less repetition. What were your thoughts
>>>>> on it?
>>>>>
>>>>> On Thu, Aug 11, 2016 at 12:23 AM, /#!/JoePea <joe at trusktr.io> wrote:
>>>>>
>>>>>> Alright, so I believe I have found the solution. It is not possible
>>>>>> to guarantee a certain module evaluation order, but using some clever (but
>>>>>> tedious) conditional checking I believe the problem is solved (with two
>>>>>> caveats listed after):
>>>>>>
>>>>>> ```js
>>>>>> // --- Entrypoint
>>>>>> import A from './A'
>>>>>> console.log('Entrypoint', new A)
>>>>>> ```
>>>>>>
>>>>>> ```js
>>>>>> // --- Module A
>>>>>>
>>>>>> import C from './C'
>>>>>> import {setUpB} from './B'
>>>>>>
>>>>>> let A
>>>>>>
>>>>>> export
>>>>>> function setUpA(C) {
>>>>>>
>>>>>>     if (!A) {
>>>>>>         A = class A extends C {
>>>>>>             // ...
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>> }
>>>>>>
>>>>>> if (setUpA && C) setUpA(C)
>>>>>> if (setUpB && C) setUpB(C)
>>>>>>
>>>>>> export {A as default}
>>>>>> ```
>>>>>>
>>>>>> ```js
>>>>>> // --- Module B
>>>>>>
>>>>>> import C from './C'
>>>>>> import {setUpA} from './A'
>>>>>>
>>>>>> let B
>>>>>>
>>>>>> export
>>>>>> function setUpB(C) {
>>>>>>
>>>>>>     if (!B) {
>>>>>>         B = class B extends C {
>>>>>>             // ...
>>>>>>         }
>>>>>>     }
>>>>>>
>>>>>> }
>>>>>>
>>>>>> if (setUpA && C) setUpA(C)
>>>>>> if (setUpB && C) setUpB(C)
>>>>>>
>>>>>> export {B as default}
>>>>>> ```
>>>>>>
>>>>>> ```js
>>>>>> // --- Module C
>>>>>>
>>>>>> import A, {setUpA} from './A'
>>>>>> import B, {setUpB} from './B'
>>>>>>
>>>>>> class C {
>>>>>>     constructor() {
>>>>>>         // this may run later, after all three modules are evaluated,
>>>>>> or
>>>>>>         // possibly never.
>>>>>>         console.log(A)
>>>>>>         console.log(B)
>>>>>>     }
>>>>>> }
>>>>>>
>>>>>> if (setUpA && C) setUpA(C)
>>>>>> if (setUpB && C) setUpB(C)
>>>>>>
>>>>>> export {C as default}
>>>>>> ```
>>>>>>
>>>>>> The caveat is that this fails in both Babel environments and in
>>>>>> Rollup. For it to work in Babel environments, `let A` and `let B` have to
>>>>>> be changed to `let A = A` and `let B = B`, as per the [fault in Babel's
>>>>>> ES2015-to-CommonJS implementation](https://github
>>>>>> .com/meteor/meteor/issues/7621#issuecomment-238992688) pointed out
>>>>>> by Ben Newman. And it fails in Rollup because Rollup [isn't really creating
>>>>>> live bindings](https://github.com/rollup/rollup/issues/845), which
>>>>>> is fine in most cases, but doesn't work with these circular dependencies.
>>>>>> The Rollup output does not create the C reference before it is ever used in
>>>>>> the first pair of conditional checks, unlike what (I think) would happen
>>>>>> with real live bindings (please correct me if wrong). To understand what I
>>>>>> mean in the case of Rollup, just run `if (FOO) console.log(FOO)` in your
>>>>>> console, and you'll get an error because FOO is not defined. Had the
>>>>>> bindings been live, then FOO *would* be defined.
>>>>>>
>>>>>>
>>>>>>
>>>>>> */#!/*JoePea
>>>>>>
>>>>>> On Wed, Aug 10, 2016 at 5:04 PM, /#!/JoePea <joe at trusktr.io> wrote:
>>>>>>
>>>>>>> I found a solution that works in environments compiled by Babel,
>>>>>>> using the [workaround suggested by Ben Newman](
>>>>>>> https://github.com/meteor/meteor/issues/7621#issuecomment-238992688
>>>>>>> ):
>>>>>>>
>>>>>>> ```js
>>>>>>> // --- Module A
>>>>>>>
>>>>>>> import C from './C'
>>>>>>>
>>>>>>> let A = A // @benjamn's workaround applied
>>>>>>>
>>>>>>> export
>>>>>>> function setUpA(C) {
>>>>>>>
>>>>>>>     A = class A extends C {
>>>>>>>         // ...
>>>>>>>     }
>>>>>>>
>>>>>>> }
>>>>>>>
>>>>>>> export {A as default}
>>>>>>> ```
>>>>>>>
>>>>>>> ```js
>>>>>>> // --- Module B
>>>>>>>
>>>>>>> import C from './C'
>>>>>>>
>>>>>>> let B = B // @benjamn's workaround applied
>>>>>>>
>>>>>>> export
>>>>>>> function setUpB(C) {
>>>>>>>
>>>>>>>     B = class B extends C {
>>>>>>>         // ...
>>>>>>>     }
>>>>>>>
>>>>>>> }
>>>>>>>
>>>>>>> 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)
>>>>>>> setUpB(C)
>>>>>>>
>>>>>>> export {C as default}
>>>>>>> ```
>>>>>>>
>>>>>>> ```js
>>>>>>> // --- Entrypoint
>>>>>>>
>>>>>>> import A from './A'
>>>>>>> console.log('Entrypoint', new A) // runs the console.logs in the C
>>>>>>> constructor.
>>>>>>> ```
>>>>>>>
>>>>>>>
>>>>>>> Although that works in my environment which is compiled from ES6
>>>>>>> modules to CommonJS by Babel, it [doesn't work in Rollup.js](
>>>>>>> http://goo.gl/PXXBKI), and may not work in other ES6 module
>>>>>>> implementations.
>>>>>>>
>>>>>>> Is there some solution that will theoretically work in any ES6
>>>>>>> module environment?
>>>>>>>
>>>>>>> */#!/*JoePea
>>>>>>>
>>>>>>
>>>>>>
>>>>>> _______________________________________________
>>>>>> 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/20161008/2fd2c503/attachment-0001.html>


More information about the es-discuss mailing list