Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relationship with the mixins proposal #33

Open
littledan opened this issue Jul 16, 2018 · 10 comments
Open

Relationship with the mixins proposal #33

littledan opened this issue Jul 16, 2018 · 10 comments

Comments

@littledan
Copy link
Member

@justinfagnani's mixins proposal fits somewhere in the same problem space as protocols, but presents a very different mechanism. How should we decide between these two proposals, something in the middle, or both coexisting?

@littledan littledan changed the title Comparison with the mixins proposal Relationship with the mixins proposal Jul 16, 2018
@ljharb
Copy link
Member

ljharb commented Jul 16, 2018

See also, #29.

@michaelficarra
Copy link
Member

A few months ago, I sent @justinfagnani this gist: https://gist.github.com/michaelficarra/ba35dcd316cd743bcdacd89bf0204390

This proposal has changed a bit since then, but I believe my personal opinion on the matter remains the same. I will try to address the topic during my status update presentation later this month.

@justinfagnani
Copy link

Sorry, I was away getting married when that gist was sent to me and it dropped though the cracks. Getting back to it now.

So that gist has a problem with how it describes the desugaring and mixin function, such that it leaves out the main benefit of subclass factories - that they don't copy members, they create a true subclass and therefore all class feature just work, like constructors, super calls, etc.

Specifically, this function is incorrect:

function mixin(C, ...Ms) {
  if (C == null) {
    C = class {};
    Object.setPrototypeOf(C.prototype, null);
  }
  return Ms.reduce((superclass, M) => {
    class D extends superclass {}
    Object.assign(D, M);
    Object.defineProperties(D.prototype, Object.getOwnPropertyDescriptors(M.prototype));
    return D;
  }, C);
}

And should be more like:

function mixin(C, ...Ms) {
  if (C == null) {
    C = class {};
    Object.setPrototypeOf(C.prototype, null);
  }
  return Ms.reduce((superclass, M) => M(superclass), C);
}

By not copying mixin members, mixins answer some of the open questions on protocols:

Should implementing a protocol actually copy symbols to prototype/constructor or use internal slots for resolution?

Mixins use the prototype chain, so neither copy, nor require internal slots.

Is there a way we can make super properties and super calls work?

super just works with mixins. So do constructors, private fields and static members.

@littledan
Copy link
Member Author

Thanks for the updates, @michaelficarra and @justinfagnani . I'm looking forward to the update in committee. Ultimately, I hope we can figure out this question before advancing either proposal to Stage 2.

@justinfagnani
Copy link

Stepping back a little bit, I think we can break down the Protocols proposal into three parts (but correct me if this seems wrong):

  1. Partial class implementations
  2. Automatic namespacing of member names
  3. Shallow type-checking

Mixins only cover the first part. This is intentional too, since they build on classes, any name-spacing or type-checking added to classes would be assumed by mixins. I would argue that mixins do a more complete job of partial class implementations precisely because they are nothing more than subclass factories.

If the other two features are deemed important, I think we could address them separately and in a way that also applies to classes, increasing their benefit.

For automatic namespacing, I can think of two ways we could achieve something similar:

1: Extra syntax for declaring and using namespaced members:

We're running out of symbols to use for sigils to denote that a member declaration should be namespaced, but we still have some syntactic room in the property name. I think would could use a dot-notation to indicate a namespace, where A.b as a property name within the class body for A would mean to define the a symbol on the property A.b and use that as the member name. Outside the class body of A, it would be a reference to A.b:

class A {
  A.foo() { ... }
}

class B {
  A.foo() { ... }
}

Would desugar to:

let A = (() => {
  const A$foo = Symbol();
  return class A {
    [A$foo]() { ...}
  }
})();

class B {
  [A.foo]() { ... }
}

2: Use decorators and computed properties.

Decorators let use

class A {
  @namespaced foo() {...}
}

class B {
  [A.foo]() { ...}
}

For type-checking, that's a much bigger can of worms, but decorators will let us do the shallow type checking pretty easily. A full example with decorators:

@abstract
class A {
  @abstract
  @namespaced
  foo() {}
}

@implements(A)
class B {
  [A.foo]() {...}
}

However those could be accomplished, I do think it would be very beneficial to tease apart the pieces of protocols so they can be applied to classes and mixins.

@littledan
Copy link
Member Author

+1 to @justinfagnani 's option 2. If decorators reaches Stage 3, let's give ourselves some time with decorators to see if this idiom is sufficient before jumping into making additional syntax for namespacing, whether it's through protocols or some other syntax like 1.

@waldemarhorwat
Copy link

@justinfagnani 's option 1 would rapidly run into syntax issues. It works if the thing before the dot is just an identifier (or an otherwise reserved word — there are no reserved words in that position!), but you'd need to allow subexpressions there too, for the cases in class B where class A is not reachable via a simple identifier. Subexpressions there wouldn't play nicely with other syntax or class semicolon insertion.

@justinfagnani
Copy link

@waldemarhorwat ah, very true. For referencing a namespaced name, a computed property name like [A.foo] would work then, which means that declaring one with A.foo would make A. nothing more than a different sigil - the name before the dot wouldn't really be significant.

@michaelficarra
Copy link
Member

@justinfagnani

Specifically, this function is incorrect:

I'm pretty sure they're exactly the same except your version pushes some boilerplate (the function wrapper and explicit extends) to the user. Contrast:

class M0 { /* ... */ }
class M1 { /* ... */ }
class M2 { /* ... */ }

class C extends myMixin(Object, M0, M1, M2) { /* ... */ }

with

const M0 = S => class M0 extends S { /* ... */ }
const M1 = S => class M1 extends S { /* ... */ }
const M2 = S => class M2 extends S { /* ... */ }

class C extends yourMixin(Object, M0, M1, M2) { /* ... */ }

The resulting C has the same prototype structure.


Regarding an entirely decorators-based implementation, I think this is probably possible but would be less elegant and lead to lower adoption. If we feel this paradigm is important enough to encourage its use, we should give it its own space in the language.

@justinfagnani
Copy link

@michaelficarra those examples are incorrect though. In the mixin pattern and the mixin proposal, mixins are not classes - they are subclass factory functions and return new subclasses each invocation.

The reason they aren't classes is that we don't want copy semantics, as that breaks the prototype chain and the ability to use super.

The examples in today's syntax, without the proposal, would be:

const M0 = (superclass) => class extends superclass {
  foo() { console.log('M0.foo'); }
}

const M1 = (superclass) => class extends superclass {
  foo() {
    super.foo();
    console.log('M1.foo');
  }
}

class C extends M1(M0(Object)) {
  foo() {
    super.foo();
    console.log('C.foo');
  }
}

const c = new C();
c.foo();

Which prints:

M0.foo
M1.foo
C.foo

With the mixin function that you have and M0 and M1 as classes, not functions, we get an exception:

Uncaught TypeError: (intermediate value).foo is not a function

This is why it's critically important that in any mixin function (which isn't really necessary, as the function application syntax is very clear, IMO), that mixins are applied by actually invoking them, creating a new subclass and prototype, as in:

return Ms.reduce((superclass, M) => M(superclass), C);

This is also why mixins in the proposal are actual functions, and class C extends Object with M1, M0 {} is doing function application to build the superclass.

With the mixins proposal the example would be:

mixin M0 {
  foo() { console.log('M0.foo'); }
}

mixin M1 {
  foo() {
    super.foo();
    console.log('M1.foo');
  }
}

class C Object with M0, M1 {
  foo() {
    super.foo();
    console.log('C.foo');
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants