Skip to content

Ergonomics at price of semantic complexity #7

Description

@keithamus

I wanted to voice my reservations around this syntax, to try to re-articulate what I was stating at TC39.

Let me put it out there first that I am definitely for some kind of private field access in both Classes and Object Literals. I think contrary to some of the commentary; having private fields in Object literals would be a welcome change.

Having said that I think this proposal points things in the wrong direction. It tries to borrow parts from two separate places (Class private fields, and Lexical Scope Semantics), but it changes the rules of these in significant ways which hurt the mental model of the language. I also don't think the pedagogical ramifications are worth the trade-off.

As I was saying to you after the March meeting, there is a trade-off between ergonomics and complexity of the mental model. By adding new syntax I hope we can simplify the mental model of the language. Case in point think private fields increases ergonomics and simplifies the mental model; because it reduces interaction and uncertainty (and therefore bounds checks) around a WeakMap. Users can forget about WeakMap, effectively dropping it from their mental model, and the additional complexity burden is simply remembering an additional # sigil. If private declarations come as a layer over private fields then they increase the complexity trade-off.

The two "Alternative" sketches you proposed, I feel have fairly large issues:

Concerns with "Alternative 1" Sketch

private #greeting
class Hello {
  #greeting = 'Hello'
  #place = 'world'
  say() {
    return `${this.#greeting} ${this.#place}!`
  }
}

This mirrors the semantics of lexical scoping, but comes at a cost - which is that the existing Stage 3 private fields proposal runs contrary to lexical scoping rules. It already comes with the implicit knowledge that #place = is scoped to the class. This proposal now adds additional layering to suggest that #place = inside of a class could mean "private" or "shared private" - and in fact there is no way to ascertain this just looking inside of the class:

class Hello {
  #place = 'world' // is this private to just me? Or shared with others? No way to tell without additional context
}

While this is also true of variable bindings - there is an implied contract that the user simply needs to look up to find the nearest const, let or var; this doesn't exist with private names.. If proposal advances based on this step, I can for-see a future where users will practice cargo cult programming and simply defensively always use private #name inside classes, which will be a loss for the ergonomic wins of private fields.

This is however a minor issue compared to alternative 2:

Concerns "Alternative 2" Sketch

private #greeting
class Hello {
  outer #greeting = 'Hello'
  #place = 'world'
  say() {
    return `${this.#greeting} ${this.#place}!`
  }
}

This example runs contrary to lexical scoping semantics, because the outer keyword is doing the hole punching - so this pattern is the inverse of what we already have learned with let/const/var in that private names are implicitly scoped unless preceded with a keyword stating otherwise. While this makes classes require less context, it goes even further to complicate the mental model by requiring developers to learn a "new thing" which on the surface looks like lexical scoping but is almost the opposite. Not only are developers required to learn the "new thing" but they also have to switch contexts between private declarations and variable bindings. I believe this sketch will be far too mentally taxing and far too difficult to teach to be useful.

A suggestion for a way forward

I humbly suggest it might be better for us to move away from borrowing from both lexical scoping and private fields. I think it might be better to move away entirely from one or the other. Might I suggest a pattern in which borrows from Symbols instead; private foo creates a private Symbol-like value which can be used with computed property access, like so:

private greeting // this acts like a Symbol but cannot be Reflect upon with `getOwnPropertySymbols`

class Hello {
  [greeting] = 'Hello' // regular computed property access
  #place = 'world' // regular private fields proposal
  say() {
    return `${this.[greeting]} ${this.#place}!`
  }
}

This suggestion does not run contrary to lexical scoping rules; private foo could be used anywhere that const foo = Symbol() could be. Property access leans on already existing semantics, so there is nothing new for developers to learn there. The only additional learning is that private foo will create a Private Symbol (a Symbol that cannot be reflected upon). This proposal does not alter private fields in any way, and as such does increase the complexity of private fields.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions