Classes referring mutually to each other

86 views
Skip to first unread message

Sébastien Doeraene

unread,
Mar 12, 2015, 6:58:43 AM3/12/15
to streng...@googlegroups.com
Hello all,

From the strawman proposal, it is unclear how classes referring mutually to each other--i.e., a method of one creates a new instance of the other--could be written. It is very clear that functions that want to mutually call each other must be defined next to each other. Should classes also do the same? Is it possible to define functions *and* classes mutually referring to each other?

For example, is the following legal?

function foo(b) {
  return new A(b.bar);
}
class A {
  constructor(x) {
    this.baz = x;
  }
  babar() {
    return new B(5);
  }
}
class B {
  constructor(x) {
    this.bar = x;
  }
  foobar() {
    return foo(this);
  }
}


In the above example, a method of B calls the global function foo, which instantiates an A, whose method babar instantiates a B. There is no cyclic dependency at runtime, because only when one calls b.foobar() do we need foo and A to be defined, and only when one calls a.babar() do we need B to be defined. But statically and textually, there is an unavoidable cycle in the references.

Cheers,
Sébastien

Andreas Rossberg

unread,
Mar 12, 2015, 7:48:49 AM3/12/15
to Sébastien Doeraene, streng...@googlegroups.com
On 12 March 2015 at 11:58, Sébastien Doeraene <sjrdo...@gmail.com> wrote:
From the strawman proposal, it is unclear how classes referring mutually to each other--i.e., a method of one creates a new instance of the other--could be written. It is very clear that functions that want to mutually call each other must be defined next to each other. Should classes also do the same?

Indeed, you are right, we should treat consecutive class declarations as mutually recursive -- in a suitably restricted manner. Obviously, the recursion at least has to go through a method of these classes, to avoid immediate errors like

class D extends C {}  // throws in ES6
class C {}

However, it's actually a bit more tricky than that. Consider:

class C { d() { return D } }
class D extends (new C).d() {}

I have some ideas how the semantics could be relaxed to be more permissive while still detecting such cases, but it's not there yet.


Is it possible to define functions *and* classes mutually referring to each other?

That would be a possible generalisation, though potentially yet more cumbersome to specify. I would be inclined to leave that out, at least for now, until we know better what common patterns will emerge with ES6 classes. Do you have an immediate use case that would be hard to refactor?

/Andreas

Sébastien Doeraene

unread,
Mar 12, 2015, 8:53:41 AM3/12/15
to Andreas Rossberg, streng...@googlegroups.com
Hi,

However, it's actually a bit more tricky than that. Consider:

class C { d() { return D } }
class D extends (new C).d() {}

I have some ideas how the semantics could be relaxed to be more permissive while still detecting such cases, but it's not there yet.

Indeed, that's a tricky one.
 
Is it possible to define functions *and* classes mutually referring to each other?

That would be a possible generalisation, though potentially yet more cumbersome to specify. I would be inclined to leave that out, at least for now, until we know better what common patterns will emerge with ES6 classes. Do you have an immediate use case that would be hard to refactor?

Yes, I do. The story is that I am the author of Scala.js, the Scala to JavaScript compiler [1], and I'm in the process of porting the back-end to Strong Mode. In general, Strong Mode looks like a perfect target for Scala.js--it already emits code that is semantically very close to it, and for most of the discrepancies, I have a quite clear vision on how to adapt things. About this, though, there are static methods (which become global functions) and classes, and all of them can mutually reference each other (and they do). I am fine with bundling all the functions and classes next to each other. But I don't see how I would work around not being able to define mutually recursive functions and classes.

At least, I am absolutely certain that without mutually recursive classes, I'll have to abandon completely. I have a feeling that top-level functions *could* be encoded as methods of a class StaticMethods with a singleton instance, but I have a hard time coming up with an initialization step for this singleton: I always end up in a cyclic dependency between the singleton's class, the 'let/const' holding its instance, and the other classes.

I understand the definition of mutually recursive classes will complicate things. I don't see why functions and classes together would complicate them *further*, though.

Cheers,
Sébastien


Sébastien Doeraene

unread,
Mar 12, 2015, 9:05:42 AM3/12/15
to Andreas Rossberg, streng...@googlegroups.com
Ah, I found an encoding for the singleton, but it's really ugly: I need to duplicate the logic for its lazy initialization at every use site :-s (I cannot factor it out as a function, because, well, the function cannot be recursive with the classes ...). For the snippet I showed above, the encoding would be the following:

let Statics = null;
class StaticsClass {
  foo(b) {
    return new A(b.bar);
  }
}
class A {
  // same as before
}
class B {
  // constructor same as before
  foobar() {
    return (Statics || (Statics = new StaticsClass())).foo(this);
  }
}

Not only is the call site of the static function very ugly, but it will also be dramatically inefficient compared to calling a top-level function, which can be resolved lexically. That kind of defeats the purpose of going to Strong Mode, IMO.

Cheers,
Sébastien

Andreas Rossberg

unread,
Mar 12, 2015, 10:06:44 AM3/12/15
to Sébastien Doeraene, streng...@googlegroups.com
On 12 March 2015 at 13:53, Sébastien Doeraene <sjrdo...@gmail.com> wrote:

Is it possible to define functions *and* classes mutually referring to each other?

That would be a possible generalisation, though potentially yet more cumbersome to specify. I would be inclined to leave that out, at least for now, until we know better what common patterns will emerge with ES6 classes. Do you have an immediate use case that would be hard to refactor?

Yes, I do. The story is that I am the author of Scala.js, the Scala to JavaScript compiler [1], and I'm in the process of porting the back-end to Strong Mode. In general, Strong Mode looks like a perfect target for Scala.js--it already emits code that is semantically very close to it, and for most of the discrepancies, I have a quite clear vision on how to adapt things.

Cool, that's exciting! Please keep us posted.
 
About this, though, there are static methods (which become global functions) and classes, and all of them can mutually reference each other (and they do). I am fine with bundling all the functions and classes next to each other. But I don't see how I would work around not being able to define mutually recursive functions and classes.

At least, I am absolutely certain that without mutually recursive classes, I'll have to abandon completely. I have a feeling that top-level functions *could* be encoded as methods of a class StaticMethods with a singleton instance, but I have a hard time coming up with an initialization step for this singleton: I always end up in a cyclic dependency between the singleton's class, the 'let/const' holding its instance, and the other classes.

Hm, given recursive classes, couldn't you trivially encode this using a class with a static method? E.g. for your example:

class foo {
  static foo(b) {
    return new A(b.bar);
  }
}
class A {
  constructor(x) {
    this.baz = x;
  }
  babar() {
    return new B(5);
  }
}
class B {
  constructor(x) {
    this.bar = x;
  }
  foobar() {
    return foo.foo(this);
  }
}
 
I understand the definition of mutually recursive classes will complicate things. I don't see why functions and classes together would complicate them *further*, though.

Maybe not, we'll have to see.

/Andreas

Sébastien Doeraene

unread,
Mar 12, 2015, 10:17:32 AM3/12/15
to Andreas Rossberg, streng...@googlegroups.com
Hi,

On Thu, Mar 12, 2015 at 3:06 PM, Andreas Rossberg <ross...@google.com> wrote:

Yes, I do. The story is that I am the author of Scala.js, the Scala to JavaScript compiler [1], and I'm in the process of porting the back-end to Strong Mode. In general, Strong Mode looks like a perfect target for Scala.js--it already emits code that is semantically very close to it, and for most of the discrepancies, I have a quite clear vision on how to adapt things.

Cool, that's exciting! Please keep us posted.

I will!
 
Hm, given recursive classes, couldn't you trivially encode this using a class with a static method? E.g. for your example:

Yes, you are absolutely right. I was misguided into thinking static methods did not exist in ES6 by reading this (obsolete) proposal:
http://wiki.ecmascript.org/doku.php?id=strawman:maximally_minimal_classes
I didn't think of checking in the latest specification draft. My bad.

So yes, recursive classes are definitely sufficient, in that case. Good! No need to take mutual function-class recursion into account.

Cheers,
Sébastien

Sébastien Doeraene

unread,
May 9, 2015, 1:01:23 PM5/9/15
to Andreas Rossberg, streng...@googlegroups.com
Hello,

I see that the strawman was updated wrt this discussion. It says (in Scoping):

For classes, forward references are only legal from within methods, and only if the referenced class does not have a (direct or indirect) backwards reference to the original class outside any method.

IIUC, this still does not allow a useful top-level-only pattern:

class Parent {
  foo() {
    return new Child();
  }
}
class Child extends Parent {}

The forward reference in Parent.foo() to Child would not be allowed, because the referenced class (Child) does have a backwards reference to Parent outside any method, namely in its extends clause.

Now, this should be a valid pattern. The cycle is not a problem in practice, because you can still create the class Child without invoking any method of Parent, that would cause a definition-time cyclic dependency.

The pattern is for example necessary to implement a functional List:

class List {
  prepend(elem) {
    return new Cons(elem, this);
  }
}
class Nil extends List {
  isEmpty() { return true; }
}
class Cons extends List {
  constructor(head, tail) {
    super();
    this.head = head;
    this.tail = tail;
  }
  isEmpty() { return false; }
}


I'm sure there are many other cases of such a cycle in common class-based code.

I understand why this clause was added. It addresses Andreas' problematic snippet above, which I repeat here for convenience:

class C { d() { return D } }
class D extends (new C).d() {}


Could we relax the wording a little bit further to allow backwards references *if* it is directly as an IdentifierReference in the ClassHeritage of the class?

Sébastien

Andreas Rossberg

unread,
May 12, 2015, 7:20:15 AM5/12/15
to Sébastien Doeraene, streng...@googlegroups.com
On 9 May 2015 at 19:01, Sébastien Doeraene <sjrdo...@gmail.com> wrote:
I see that the strawman was updated wrt this discussion. It says (in Scoping):

For classes, forward references are only legal from within methods, and only if the referenced class does not have a (direct or indirect) backwards reference to the original class outside any method.

IIUC, this still does not allow a useful top-level-only pattern:

class Parent {
  foo() {
    return new Child();
  }
}
class Child extends Parent {}

Yes, I agree that this is a case that should be allowed. The current spec certainly isn't supposed to be the last word on this subject, but meant as a step in the right direction. :)

 
The pattern is for example necessary to implement a functional List:

class List {
  prepend(elem) {
    return new Cons(elem, this);
  }
}
class Nil extends List {
  isEmpty() { return true; }
}
class Cons extends List {
  constructor(head, tail) {
    super();
    this.head = head;
    this.tail = tail;
  }
  isEmpty() { return false; }
}

Indeed, thanks for this example.
 

I understand why this clause was added. It addresses Andreas' problematic snippet above, which I repeat here for convenience:

class C { d() { return D } }
class D extends (new C).d() {}


Could we relax the wording a little bit further to allow backwards references *if* it is directly as an IdentifierReference in the ClassHeritage of the class?

Yes, something along these lines should be possible. It's a bit ad hoc, though, and would still break certain abstractions, like wrapping a function call around the extended class (which is something you might want e.g. for mixin patterns). But that probably can't be helped no matter what.

Anyway, I agree completely, but we're still trying to find the right balance between simplicity, ease of checking, and generality. Roughly, any backwards use of a class should be fine that is guaranteed not to cause any evaluation of user code. That includes e.g.

class D extends (class extends C { ... }) { ... }

We could basically define a grammar of safe expression contexts. But maybe there is no point in being more general than your suggestion...

/Andreas

Reply all
Reply to author
Forward
0 new messages