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

Ergonomics at price of semantic complexity #7

Open
keithamus opened this issue Apr 9, 2019 · 4 comments
Open

Ergonomics at price of semantic complexity #7

keithamus opened this issue Apr 9, 2019 · 4 comments

Comments

@keithamus
Copy link
Member

keithamus commented Apr 9, 2019

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.

@ljharb
Copy link
Member

ljharb commented Apr 9, 2019

This suggestion sounds somewhat like Justin’s proposal from January which did not achieve consensus for stage 1.

@jridgewell
Copy link
Member

Thanks for the writeup!

The only real downside in ergonomics I can see to this suggestion is that this[greeting] is a bit more difficult to understand than this.#greeting. Maybe if it were private #greeting and this[#greeting], so that it's clear I'm accessing the private symbol, though this wouldn't prevent you from doing private #greeting; const greeting = #greeting.

This suggestion sounds somewhat like Justin’s proposal from January which did not achieve consensus for stage 1.

Almost. My proposal was about changing access semantics (prototype lookup, can be installed on foreign objects without any fuss) as much as the eventual reification. Now that we've decided that branded-weakmap semantics are a requirement, we could just have a private symbol reification that acts like a branded-weakmap.

Unfortunately, that doesn't simplify a whole lot with my private symbols proposal. Proxies will again have to update their internal methods to not call traps (though now they'd throw if not installed on the proxy instead of tunneling). Additionally, we'd still need the "known private symbols" API to be added to satisfy membranes.

@keithamus
Copy link
Member Author

I hold no real opinions about the sketch I suggested, but I felt without proposing a way forward my post was just complaining about the current proposal. private #greeting/this[#greeting] is fine with me 👍

@robbiespeed
Copy link

What if we leveraged the existence of Symbols as they are today, but added a new way of using them for assignment/access of private fields. Something like:

export const GREETING = Symbol();

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

This would allow sharing of private fields across module boundaries through symbols as can be done today with non-private members. It wouldn't require use of a new keyword, and seems easily teachable as it only adds one new concept (dynamic private field accessor) that closely matches existing behaviour (dynamic property accessor).

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

4 participants