js-classes-1.1 icon indicating copy to clipboard operation
js-classes-1.1 copied to clipboard

How to critique this proposal

Open allenwb opened this issue 7 years ago • 68 comments

When reading and raising issues about the Max-Min Classes 1.1 proposal it is important to understand how it relates to the current set of class feature proposal that are at various states in the TC39 process pipeline.

This proposal is a response to the overall complexity that would be introduced into the language by the full collection of current and contemplated proposals. Basically, we don't think that the JS complexity budget for the near future is big enough to invest so much of it into class extensions. JavaScript has generally followed a minimalist design style. From that perspective, the contributors of this proposals are primarily concerned about the amount of complexity that the existing proposal would inject into the language. The complexity concern has also been raised by a number of community members.

This proposal is not a point-by-point attack of any feature of any of the current proposals. There are many good ideas in the existing proposals. Some of them we've borrowed. Instead, this proposal is an attempt to fight back against overall complexity. We are not trying to "fix" or improve other any existing proposal. It is an experiment to see where we might end up if we start with a clean slate and took a minimalist approach to adding some pretty essential functionality that is not in ES6 class definitions. We want to see where we get if following that same Max-Min methodology that enabled the introduction of class definition syntax into ES6.

This proposal is a carefully balanced set of functionality that attempts to include just enough and not too much new capability and associated new complexity. The best way to critique this proposal or understand the rationale for our decisions is to focus on the actual proposal rather than trying to do a feature-by-features bake-off against other proposals. For example, rather than asking "why did you leave out public fields?" ask "Why didn't you include a mechanism for declaring instance own properties?"

We fully expect to have to explain difference between various features of this proposal and other proposals. Just remember, that sort of point-by-point comparison is not the design methodology we have followed and may not give you the best understanding of this proposal. Try to understand it as a whole before you start the feature point comparison.

allenwb avatar Mar 12 '18 20:03 allenwb

Instead, this proposal is an attempt to fight back against overall complexity.

I definitely feel the complexity budget squeeze as the language continues growing. But... I guess I really don't share the impression that this proposal uses less of it than the existing one. It adds four new and diverse syntactical forms, one of which overloads an existing one with radically different semantics: var x, hidden foo(){} this->x, and static {}. And it fails to provide for what I think is the overwhelming majority use case, which is initializing public instance & static fields. It feels like spending more and getting less.

I agree that the important thing is to consider proposals holistically, with a particular emphasis on overall complexity. I am just confused by the implication that this proposal is less complex that the current one. To me it feels far more so.

bakkot avatar Mar 12 '18 21:03 bakkot

I guess I really don't share the impression that this proposal uses less of it than the existing one.

As @allenwb states, the comparison should not be to any single proposal in isolation, but to the full set of class features that are either in some form of development or have been deferred to future proposals. That is a large list, with noticeable unresolved problems. We see our alternative as a small, closed, self-sufficient set of essential functionality.

Of course it's not perfect (yet?), but that's not the point. The point is to see if we can create an alternative that is complete (and therefore represents less design risk), while avoiding some of the troubles that the current proposals find themselves in.

zenparsing avatar Mar 13 '18 01:03 zenparsing

One conflict here is that some of the use cases this "minimal" proposal doesn't address, are indeed essential. The complexity that the current fields proposals add tends to be deemed "very much worth it" by those who need these use cases.

If a less complex proposal can address them, then great! However, if not, then it's not actually a better proposal - it's just a simpler one, which isn't inherently better in and of itself, despite the value of minimizing complexity.

ljharb avatar Mar 13 '18 01:03 ljharb

@ljharb

some of the use cases this "minimal" proposal doesn't address, are indeed essential.

I guess it depends upon your definition of "essential". The definition I use is provided in https://github.com/zenparsing/js-classes-1.1/issues/11

But from a Max-min design approach, the meaning of essential is fairly straight forward. If all of the stake holders in the design agree that something is essential, then it is essential. If there is disagreement as to whether or not some feature or capability is essential, then for the purposes of the Max-min design it can't be considered essential.

As a starting point on what is essential, I this we have significant consensus that some mechanism that provides securely encapsulated per instance state is essential.

allenwb avatar Mar 13 '18 03:03 allenwb

Max-min motivated the ES2015 design, and while that may very well have been the right decision, I'm not sure that was in fact considered "successful" nor that max-min is automatically the right approach to take now that we have the post-ES6 process.

ljharb avatar Mar 13 '18 04:03 ljharb

This proposal is great!

It avoid the main syntax issues in current proposals:

  1. No #priv which most programmers hate. (Yes, they hate it.)
  2. No new ASI hazard. (by reuse var keyword)

It also make the internal slot (instance variable) concept much much clear by introduce -> diff from . or [].

I agree public property initialization is good to have, but not must to have in current stage. We could leave it, check how TypeScript users use the features here proposed and property initialization they supported together, then decide whether or how to introduce public property initialization in the future.

I LOVE this proposal! Thank you @zenparsing and @allenwb to bring this to us!

hax avatar Mar 13 '18 13:03 hax

Note that it's the combination of reusing the var keyword and not having any initializers that make this proposal eliminate ASI hazards.

littledan avatar Mar 13 '18 14:03 littledan

@littledan I think even having initializers it will make much less new ASI hazards than current proposals.

class A {
  var x = 0
  *gen() {}  // syntax error
  var y = 1
  [computed]() {} // syntax error, and it's not a new ASI hazard for semicolon-less coding style
}

All other weird cases are just gone.

hax avatar Mar 13 '18 16:03 hax

@zenparsing,

but to the full set of class features that are either in some form of development or have been deferred to future proposals. That is a large list, with noticeable unresolved problems

This confuses me a bit; can you be more explicit about what you're referring to?

bakkot avatar Mar 13 '18 16:03 bakkot

@bakkot

one of which overloads an existing one with radically different semantics: var x, hidden foo(){} this->x, and static {}.

  • var x: I think this syntax is very easy to understand, the old var is for function local variables, and this is for class "local" variables.
  • this->x: At least much better than #x
  • static {}: Easy to understand for Java/C# programmers. (Though C# use a little different syntax.) It seems current proposals do not have corresponding feature. As the examples showed, I see it's a useful feature, but as I understand it's not the core feature of this proposal and can be postponed, I can live without it.
  • hidden foo(){}: I wish it could be private foo() {}, but it rely on TypeScript team's decision of how to incorporate their private and js native private/hidden. So I'm fine with hidden, we already introduce keywords like yield/async/await, so it's not a big deal. Again, at least much better than #foo() {} for most programmers.

hax avatar Mar 13 '18 17:03 hax

@hax:

var x: I think this syntax is very easy to understand, the old var is for function local variables, and this is for class "local" variables.

It's not a class local variable, since a function which closes over it will see different values depending on how the function is invoked, which breaks a fundamental contract of variables in JavaScript. It's not a variable at all, in any sense. Short of eval, there's no place in the language where you can summon a new variable into being without creating a new function. It's a value associated with an object. I get why it's confusing to call it a property, but I think it's more confusing to think of it as a variable.

[etc]

I'm not saying any of these are necessarily bad, except the first. I'm just saying they collectively add a huge amount of complexity to the language. I stand by that claim.

bakkot avatar Mar 13 '18 17:03 bakkot

I'm not saying any of these are necessarily bad, except the first. I'm just saying they collectively add a huge amount of complexity to the language. I stand by that claim.

Of course this proposal adds complexity to the language. If it didn't add some complexity it would be adding any new expressibility to the language. What it tries to avoid in adding inessential complexity.

You aren't arguing that this proposal adds more complexity then then union of all the class extensions proposal that are already in the TC39 pipeline, are you?

allenwb avatar Mar 13 '18 17:03 allenwb

@allenwb,

You aren't arguing that this proposal adds more complexity then then union of all the class extensions proposal that are already in the TC39 pipeline, are you?

I can't answer this without knowing which things we're considering to be class extensions proposals. The class fields proposal, sure, but... Callable constructors? Decorators? Mixins? A future shorthand for private field access? I don't see how this proposal would rule any of those out, for example, and commentary elsewhere suggests it's not intended to. But as such I don't understand why we'd be interested in comparing the complexity cost of "this proposal" vs "the current class fields proposal, plus callable constructors, plus decorators, plus mixins, plus shorthand".

I do claim it adds more complexity that the current class fields proposal. I personally would be more than content to get just that proposal and no others, so from a max-min perspective, it seems like that proposal would be a strict improvement over this one.

bakkot avatar Mar 13 '18 18:03 bakkot

I personally would be more than content to get just that proposal and no others, so from a max-min perspective, it seems like that proposal would be a strict improvement over this one.

Let's look at a specific use case: self-hosting builtins with class definitions. The fields proposal is not sufficient in its current form to completely address that use case. It is missing the capability to (a) securely decompose methods and (b) expose access to encapsulated state outside of the class for use by "friends". If we want to take a holistic view of class features, then we must also take into account the additional proposals that would support these capabilities, along with any uncertainty or risk surrounding those features.

zenparsing avatar Mar 13 '18 18:03 zenparsing

The fields proposal is not sufficient in its current form to completely address that use case. It is missing the capability to (a) securely decompose methods and (b) expose access to encapsulated state outside of the class for use by "friends".

(a) Sure. Then let's consider as our point of comparison the class fields proposal + the private methods proposal, which is incidentally the set of stage 3 proposals touching classes.

(Although, of course, using per-instance private fields containing functions amounts to the same thing; it creates more objects, but provides the capability desired with nothing beyond the class fields proposal.)

(b) This doesn't require language level support.

let subscriptionClosed;

class Subscription {
  #state;
  static finalize() {
    subscriptionClosed = s => s.#state = 'closed';
    delete this.finalize;
  }
}
Subscription.finalize();

I don't claim it's clean, just that it's doable, and concisely.

Furthermore, if we got decorators it could be done more cleanly than this. I don't think this proposal can be considered in isolation from other proposals such as that one.

then we must also take into account the additional proposals that would support these capabilities, along with any uncertainty or risk surrounding those features.

To my eye, it looks like it requires only the class fields proposal plus the private methods proposal. The union of those two still seems much simpler than this, while providing more. Do you think there are others which are necessary for this use case, or some other specific use case? Which?

I'm sorry to push on these details so hard, but I really feel like we can't much talk about this without being explicit about what you and Allen mean by "the full set of class features that are either in some form of development or have been deferred to future proposals", and which of those you feel this proposal subsumes or renders unnecessary. As long as it's not "all proposals touching classes", which it seems it is not, then "the full set" isn't really specific enough for me to tell what you're intending to point at.

bakkot avatar Mar 13 '18 18:03 bakkot

@bakkot

which breaks a fundamental contract of variables in JavaScript

Sorry I don't get this. Could you give a small example show the breaking contract?

It's not a variable at all, in any sense... I think it's more confusing to think of it as a variable.

Of coz it's not function variable, but I don't see why "instance variable" is wrong. We already use the term "instance variable" several years in many languages and loosely in JavaScript. For example DC's very old article: https://crockford.com/javascript/private.html already use "instance variable".

hax avatar Mar 13 '18 19:03 hax

I claim that the delete trick for exposing private access is not sufficient, in the sense that it is not usable enough on its own to leave it as-is. It creates a void into which some kind of syntax must find its way. If that syntax is decorators, then we must add the decorators proposal to the "complete" class feature set.

Furthermore, there are some issues with those proposals. From a developer's point of view, does it make sense that you can have static fields and static methods but not static private? Why is a private field just a value slot, but a private method has property-descriptor-like semantics (e.g. allowing accessors)? Is exposing something like PrivateName through decorators the safest way to expose private access? Will developers get over their distaste for #?

The list above isn't meant to be a point-by-point critique of the current set of proposals. Rather, it is meant to call attention to the fact that the current direction for classes seems to imply a fairly high degree of complexity and risk.

Part of the problem is that the current approach to developing class features is so open-ended that it's hard to tell what classes will end up looking like, or what features will end up in them. Narrowing our scope helps give us confidence.

Also, we understand the disruptive nature of this proposal, and I really appreciate the feedback!

zenparsing avatar Mar 13 '18 19:03 zenparsing

I agree that we shouldn't encourage anyone to use the delete trick. I think we should work on a static blocks proposal for this purpose. Several TC39 members are talking about this and I hope we'll see a Stage 1/2 proposal soon.

littledan avatar Mar 13 '18 19:03 littledan

@hax

Sorry I don't get this. Could you give a small example show the breaking contract?

Sure, let me elaborate. JavaScript functions are lexically scoped: the variable to which a given identifier refers is determined at the time the function is defined, not the time at which it's invoked. (It's a little more complicated than that because of dynamic scope like with [in fact it's the scope chain which is fixed, not the reference resolution], but bear with me.) Some functions introduce their own variables in the form of parameters and this, but these aren't captured from an outside context, and references to these do not conceptually "leave" the function.

That means that if I have

var x;
[...]
function f(){
  [...] x [...]
}

then no matter what I do with f or how it's referring to x, that x is always the same x. It can only hold a single value: if I mutate x that change is reflected in subsequent invocations of f; I can't invoke f it in a way which changes that. This is true no matter what kind of function f is: in particular it is true for methods.

With this proposal's var x, that's not true:

class A {
  var x;
  f(){
    return this->x;
  }
}

This creates only a single function f (and some others, but they're not relevant). But the thing which x refers to there is not a lexically captured variable in any sense. There are potentially infinitely many values which can be stored in it simultaneously, and which of them you get depends on how f is invoked: calling it as a method of one object can yield a different value than calling it as another. That's what I mean.

To be more explicit:

var x = 0;
class A {
  m(){
    return x++;
  }
}
(new A).m(); // 0
(new A).m(); // 1

// vs

class A {
  var x;
  constructor(){ this->x = 0; }
  m(){
    return this->x++;
  }
}
(new A).m(); // 0
(new A).m(); // 0

The bit that confuses me here is that Allen himself has repeatedly proposed allowing variable declarations in class bodies as means of create normal variables in the scope of the class body, that is, variables which would be closed over by methods in the class body by the usual means and which have a single identity shared among all such methods and invocations thereof, including on different instances. That is after all how variables work in JavaScript.

We already use the term "instance variable" several years in many languages and loosely in JavaScript. For example DC's very old article: https://crockford.com/javascript/private.html already use "instance variable".

Having tutored a few brand-new JS programmers in the last couple years who had encountered Crockford's article or teaching materials based on them, I can now say with high confidence that that particular use of the term "variable" is extremely confusing to new programmers, and in my experience pretty much always causes them to have the wrong mental model of what's happening not just in that code but in the rest of the language.

bakkot avatar Mar 13 '18 20:03 bakkot

@zenparsing / @littledan

I claim that the delete trick for exposing private access is not sufficient, in the sense that it is not usable enough on its own to leave it as-is. It creates a void into which some kind of syntax must find its way.

I disagree. Like I say, I'd be fine with just the current fields proposal, or that + private methods. I wouldn't necessarily object to a more general construct which also satisfied that case; I just don't actually think that pattern is bad enough or use case common enough that syntactical support for something else is in fact inevitable. I think that pattern is plenty usable, especially if we're concerned about the overall complexity of the language. I don't especially like it, but I also really don't like adding more syntax, so...

@zenparsing

Furthermore, there are some issues with those proposals.

Agreed that there are some issues and edge cases, though I think they're much less of a big deal than you seem to. But this thread was mainly about complexity, and I wanted to say that this proposal seems to me to be much more complex than those do. (Also, specifically, "why can't I have private static?" seems like fundamentally less of a complexity cost than, for example, "why does this->x only sometimes do a brand check?". Code you can't write is simpler than code which requires several moving parts in a mental model to reason about.)

Part of the problem is that the current approach to developing class features is so open-ended that it's hard to tell what classes will end up looking like, or what features will end up in them. Narrowing our scope helps give us confidence.

That's why I keep asking for which specific proposals you intend this one to rule out. If it doesn't rule out callable constructors, or mixins, or decorators, or shorthand syntax for private field access, then it seems to me you have exactly the same problem: you just have the class fields and methods proposals, except with more syntax and no public fields, and no more or less of an ability to say whether any other feature will end up in classes.

That is to say, I don't think narrowing our scope actually does us give any additional confidence unless we specifically say which things are now out of scope for any future proposal. Since discussion in other threads (e.g. #33) suggests that this proposal is not intended to "finish" classes, it really feels to me like this has not been done. Absent that, if anything I think narrowing our scope gives us confidence, since we are then not considering interactions with those things which are now not in scope.

bakkot avatar Mar 13 '18 20:03 bakkot

@bakkot Ok, I see.

But I will argue, for the guys who know plain old JS

class A {
  var x;
  f(){
    return this->x;
  }
}

is just sugar of

function A () {
  var x;
  this.f = function () {
    return x;
  }
}

This is the closure private pattern from ES3 era.

I understand some newcomers can't get it in the beginning, but when they finally learn the connection between class and function, they will not expect var x in class body refer to same var, just like you can't expect var x in function body refer to same var.

Having tutored a few brand-new JS programmers in the last couple years who had encountered Crockford's article or teaching materials based on them, I can now say with high confidence that that particular use of the term "variable" is extremely confusing to new programmers, and in my experience pretty much always causes them to have the wrong mental model of what's happening not just in that code but in the rest of the language.

Yeah, I still remember when I first saw "instance variable" in a C++ book 25 years ago, I was a little confused. But unfortunately, there are many "variable" term issues in programming languages, eg. global/static modifier VS normal variable in many languages are much more confused IMO. So I don't think "instance variable" is a big deal. 😝

hax avatar Mar 13 '18 21:03 hax

BTW, @zenparsing I'm curious could we just use x as the shortcut for this->x ?

class A {
  var x
  f1() {
    return x + 1
  }
  f2() {
    const x = 2
    return this->x + x // only need this->x when there is a shadow x
  }
}

hax avatar Mar 13 '18 21:03 hax

@hax

is just sugar of

But it is not just sugar of that. There's only one function object created by the class definition. And it's especially confused given static blocks:

class A {
  static {
    console.log('reached');
  }
  var x;
  f(){
    return this->x;
  }
}

The static block runs just once at class definition time, but a new var x is created every time the class is instantiated.

bakkot avatar Mar 13 '18 21:03 bakkot

@bakkot Yes it's obviously not just sugar. This is the biggest limitation of the old closure private. My point is this proposal provide a good mapping for the old way. I don't think static is a big problem, we do not have "static" in all old pattern at all. And when ES6 introduce static method, all programmers already have to know the semantic difference between static and normal methods.

After all, var keyword usage is just a small problem IMO, I'm ok to switch to other keyword, like also use hidden or use instance. What you prefer?

hax avatar Mar 13 '18 21:03 hax

@hax

I'm curious could we just use x as the shortcut for this->x

See #18 (and feel free to continue discussion in that thread, if you like).

zenparsing avatar Mar 13 '18 21:03 zenparsing

@bakkot

which things we're considering to be class extensions proposals.

Max-min classes is an alternative to all of the following proposals: Private instance methods and accessors Class Public Instance Fields & Private Instance Fields Static class fields and private static methods Also the short hand syntax for private fields and method, which isn't yet on the official TC39 proposal page

It is orthogonal to and does not replace: Decorators but would require some modifications to that proposal. See https://github.com/zenparsing/js-classes-1.1/issues/33

It does not address one way or another functionality address by: Maximally Minimun Mixins

allenwb avatar Mar 13 '18 21:03 allenwb

@bakkot

The bit that confuses me here is that Allen himself has repeatedly proposed allowing variable declarations in class bodies as means of create normal variables in the scope of the class body,

Yes I advocated for that as an alternatively way to remediate some of the issues with the private methods and static privates proposals. That experience is one of the things that convinced me a piecemeal or kick the ball down the field approach to extending class definitions was leading to too much overall complexity. It led me to agree to participate in developing this max-min proposal. In the context of the Max-min proposal I withdraw my support for class body lexical declaration .

Your variable binding argumenbts seem predicated upon the keyword var. Do they still hold if we replace var with any other word such as xyzzy? Or just #x;.

Instance variable declarations in our proposal do not create a lexical variable binding. They reserve an instance variable "slot" (if you will) in the per instance encapsulated state and introduce a lexically scoped (and namespaced) hidden name binding. The hidden name binding mechanism is essentially the same as used in the private fields proposal.

foo->x is not a variable reference to variable x, it is largely equivalent to foo.#foo in the private name proposal. Other then repurposing of the keyword var why doesn't your arguments apply to private fields?

allenwb avatar Mar 13 '18 21:03 allenwb

@allenwb, thanks for the list.

Your variable binding argumenbts seem predicated upon the keyword var. Do they still hold if we replace var with any other word such as xyzzy? Or just #x;.

No; I'm mostly just worried about var and about the language around it. Here this was specifically responding to a comment to the effect that "the old var is for function local variables, and this is for class 'local' variables", and to @zenparsing elsewhere emphasizing the importance of considering these to be "instance variables". I continue to feel they are not in any sense variables and should not be considered as such; I would be much happier if the syntax for declaration did not imply they are, as it currently does.

bakkot avatar Mar 13 '18 22:03 bakkot

@allenwb,

Earlier you asked:

You aren't arguing that this proposal adds more complexity then then union of all the class extensions proposal that are already in the TC39 pipeline, are you?

If "the union of all the class extension proposals" is this list, then: yes, I am. A fair bit more complexity, in fact, in exchange for less usability.

bakkot avatar Mar 13 '18 22:03 bakkot

From a developer's point of view, does it make sense that you can have static fields and static methods but not static private?

I assume you mean "static properties". The max-min classes proposal does not have any declarative concept of static properties other than static methods. If you want to define a data property on the constructor you would use the static initializer in a manner that is similar to how you would define an own instance property:

class C {
   constructor () {
      this.c = C.CONSTANT;
    }
   static {
      this.CONSTANT = 42
   }
}

We considered and rejected the idea of including static instance variables. That is one of our simplifications. Observing various discussions concerning "static private fields" it was clear that to many people their semantics is unintuitive. They seem to expect that such fields would be replicated (and reinitialized??) on subclass constructors. If they are not inherited in that manner (and in a simple design they would not be) that confuses this group of people. But, to make them be inherited adds more complexity of the class creation process. Our conclusion was that static instance variables weren't useful enough to warrant either of those outcomes.

"why does this->x only sometimes do a brand check?". Whether x is an instance variable, method, or access is statically determinable (essentially the hidden name x is statically typed. So, in context, it can be statically determined whether this is an instance variable access or a function invocation (accessors are function invocations). All variable accesses in JS must be memory safe so when x is an instance variable name in foo->x an access validity check is performed. Function invocations in JS generally don't do any validity checks (other than ensuring that a valid function is being called) so foo->x() does not do extra checking after retrieving the function object, just like f() does not do an extra validity check. See the rationale document for some potential optimization that are enable by the "static typing" of hidden name references.

allenwb avatar Mar 13 '18 22:03 allenwb