tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Re-examining classes-1.1 #163

Closed rdking closed 5 years ago

rdking commented 6 years ago

Continuing from #100... @ljharb @bakkot @littledan @mbrowne

I opened this because @glen-84 was right, we needed to move the discussion to a different thread. Here's a sample of code using classes-1.1 as of the most recent discussions.

class Point2D {
  //Private members are declared with either `let` or `const`
  let x = 0;
  let y = 0;
  let polarToXY = (r, theta) => new Point2D(r * Math.cos(theta), r * Math.sin(theta));

  //Everything else is a property of either the prototype or the constructor
  readonly POLAR = Symbol("POLAR");
  readonly XY = Symbol("CARTESIAN");
  get x() { return x; }
  get y() { return y; }
  translate(type, ...args) {
    switch (type) {
      case POLAR: {
        let p = polarToXY(args[0], args[1]);
        //translate can access closures created by this class
        x += p::x;
        y += p::y;
        break;
      }
      case XY: {
        x += args[0];
        y += args[1];
      }
      default:
        throw new TypeError("Invalid coordinate type");
    }
  }
  constructor(x, y) {
    //This `scope operator` also helps solve issues of shadowing.
    this::x = x;
    this::y = y;
  }
}

In the above example, POLAR and XY are non-writable, prototype-owned public properties. The general rule is that if you want something private, use let or const. Otherwise, you're declaring class product properties.

I'm writing up a proposal for the readonly modifier as something separate since it is separately useful in the same way as get & set. I'm considering whether this proposal should also include a final to set configurable=false and a hidden to set enumerable=false.

IMO, this doesn't lose any elegance at all compared to the original 1.1 proposal. Even if some of you think it does, certainly it still retains more elegance than can be found in the existing proposal. In either case, since this version of the proposal follows the closure paradigm instead of the WeakMap paradigm, it won't suffer any of the corresponding issues with WeakMap.

Private members are distinctly not properties of the instance that owns them. The non-prototype, non-constructor definitions within the class represent a closure (function scope) definition much like that of a function, with the exception that in this closure, you can only define variables. The property members of the class are also members of the closure. This allows the functions to access elements of the closure as if they were declare as nested within the closure.

Upon instantiation of a class instance, the closure definition is used to create a closure that is attached to the instance object. The new :: grants access to any object's closure from within a function known to the target closure's definition. So by a similar method as class-fields, access to the private data of an instance is protected.

mbrowne commented 6 years ago

@rdking Are zenparsing (Kevin Smith) and other supporters of classes 1.1 on board with this new version? If so, great. If not, there should probably be a new name for this version of the proposal to avoid confusion with the original classes 1.1.

rdking commented 6 years ago

I'm not yet certain where @zenparsing stands on this variation. I know he seemed to be along for the ride up to the addition of public data properties, but I haven't heard his opinion of this yet. If he doesn't like it, I'll gladly fork and rename it.

rdking commented 6 years ago

After thinking about it for a while, I decided to fork classes-1.1 anyway. That way it'll be perfectly clear that this new proposal contains alterations that were not present in the original proposal that should make it a more viable candidate and worthy of re-evaluation.

mbrowne commented 6 years ago

@rdking

since this version of the proposal follows the closure paradigm instead of the WeakMap paradigm, it won't suffer any of the corresponding issues with WeakMap.

I just realized that I might have read this backwards when I read it for the first time... are you saying that the class fields proposal follows the WeakMap proposal whereas the https://github.com/rdking/proposal-class-members proposal follows the closure paradigm? It seems like a bit of an odd statement since (the private fields part of) the class fields proposal also has a lot in common with closure semantics. In any case, as far as I can see both of these paradigms offer complete hard privacy, so I assume you were referring to something else when you said "issues with WeakMap".

rdking commented 6 years ago

In truth, the WeakMap paradigm requires use of a closure to work. Otherwise, the data isn't private.

const A = (function() {
   let pvt = new WeakMap();
   return class A { ... };
})()

The point of the WeakMap is to act as a container for instance-specific data. With class-members, I'm taking more of a closure-only approach.

const A = function() {
  ...
  return { .... }
}

The premise is that creating an instance creates a function closure around the instance and it's prototype functions. Creating a class creates a function closure around the entire class and its defined functions. Private members live in these closures.

rdking commented 6 years ago

The issue with WeakMap was in reference to Proxy. After yet another long discussion with @ljharb, it seems that TC39 intentionally made Proxy underpowered. That's part of the reason why if you wrap a Proxy around anything that keys a WeakMap against the target object's identity, instead of unwrapping the Proxy, it tries to simply look up the Proxy itself.

There's a simple way to work around this that works for all cases of WeakMap and Proxy. For class-fields, however, the work-around would need to be built into the proposal. The same is also true for my proposal, but I already have a way to resolve this. I'm just not sure that TC39 would accept such a thing, and @ljharb is almost absolutely certain they wouldn't.

ljharb commented 6 years ago

Please don’t mischaracterize me; i never said Proxy was intentionally made “underpowered”. What i said was that the primary intention of Proxy was to enable a userland membranes implementation to be possible.

Separately, it’s not that I’m certain such a change wouldn’t be accepted; it’s that I’m certain it’s both large enough of a change to warrant a separate proposal, and also that the current class fields proposal does not preclude that future change.

rdking commented 6 years ago

Sorry if that was a mischaracterization. Thanks for the clarification.

rdking commented 6 years ago

@ljharb I still don't understand why something that wouldn't at all modify Proxy semantics or invariance require a separate proposal when it can be trivially shown to be critical to the functioning of various libraries that Proxy doesn't interfere. Can you elaborate?

ljharb commented 6 years ago

Any “difficulties” ascribed to Proxy and private fields predate private fields; at worst, private fields increases the surface area of the problem. A solution that only addressed private fields and not the other places in the language with similar behavior would be incomplete.

rdking commented 6 years ago

I guess that's, once again, 2 different perspectives.

littledan commented 5 years ago

I think we've had plenty of discussion about Classes 1.1; we seem to be covering points here that have already been thoroughly discussed in other threads.