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

/#!/JoePea joe at trusktr.io
Wed Aug 24 03:17:42 UTC 2016


> 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/met
>>>>> eor/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/20160823/90b15df8/attachment-0001.html>


More information about the es-discuss mailing list