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

Please do not use "#" #77

Closed babakness closed 6 years ago

babakness commented 6 years ago

Two reasons taste and utility.

  1. Taste: "Reason is the slave of passion" - Hume.

So I confess that to me, this is just straight up ugly. Can there be something other than the '#' character? Why not just a keyword

class {
  whatever x = 5
}

Where "whatever" is a clean word that anyone reading would understand and is also not painful to the eyes.

  1. Utility. There could be a really awesome use of "#" in the future, lets not throw away that opportunity here.
ljharb commented 6 years ago

Please read the FAQ; you'll hopefully see why no keyword is feasible.

As for your personal taste; "ugly" is subjective, and many don't find it ugly.

babakness commented 6 years ago

Ok, subjectivity aside, # could have great potential for something in the future. Its a very assessable character. Right there above the middle finger.

This is an edge case concern. I can appreciate the need to have truly private attributes. Why not ### instead of # given the far reaching concerns and utility a single # can offer.

babakness commented 6 years ago

Consider, for example, that # in Scala is used for accessing nested classes. My point is that it is important to really consider the future of the language the ergonomics that are being forfeited. The FAQ is also full of opinions, for example, it is stated that this is confusing. I disagree, it is merely an implicit parameter rather than explicit as it is in python (self). Furthermore this is Javascript, it's a necessary learning curve.

I have to admit that having a this.bar inside of class and not a function is confusing in that prototypes are kind of masquerading as classes. Here, this isn't an implicit parameter, so yeah, its confusing.

This need for # is suspicious however. It feel that there is this supposed pressing need for the world that a keyword is just going to really slow things down. Why? Couldn't it be argued that a keyword could become # behind the scenes? So therefore # could really be anything, ###, or whatever.

A keyword is plausible. It is clearer, easier to read, easier to teach, and better to look than a misplaced # tattoo.

ljharb commented 6 years ago

A keyword used for declaration that is not also used for access won’t work (per the FAQ).

Do you have another single-character syntactic suggestion besides #? Multiple chars would just make this important feature (way more important than accessing nested classes, for example) less ergonomic.

babakness commented 6 years ago

Imagine if async/await was paired with symbols instead. Let's say #% and #$; so that one might write #%*function() { const foo = #$ bar() } or if the setters and getters were done using bizarre symbolic contraptions.

Of course keywords are better. A keyword for generators would have been even better. What follows is actual Perl code.

($l=join("",<>))=~s/.*\n/index($`,$&)>=$[||print$&/ge;

Let's stop making Javascript a write-only language. Adding these kinds of symbols is damaging to be sure. Perl went down this "developer ergonomics" rabbit hole. The aftermath of Perl attracted many to following wisdom

https://www.python.org/dev/peps/pep-0020/

# as proposed here is not easy to read, not easy to explain, not explicit, and inconsistent. In fact the problem the FAQ mentions about this are because it is implicit rather than explicit. The implicit behavior of this makes it difficult to teach. It is therefore ironic that # is suggested instead.


class Foo {
   const private bar = 5
   let private baz = 6
   method() {
     console.log( private bar)
     private baz = 7
     // private bar = 8 throws exception (const)
   }
}

What's wrong with that?

ljharb commented 6 years ago

It doesn’t allow you to access private values on other objects besides this.

I’d really suggest rereading the FAQ; all of your reasoning is sound, but seems to be missing some of the constraints.

babakness commented 6 years ago

I see this in the FAQ

this is already a source of enough confusion in JS; we'd prefer not to make it worse. Also, it's a major refactoring hazard: it would be surprising if this.x had different semantics from const thiz = this; thiz.x.

This also wouldn't allow accessing fields of objects other than this.

Those words are there just hanging around, no examples. Please explain and amend the FAQ to state the issue clearly with code so that one can reason about what exactly is the problem. Thanks.

ljharb commented 6 years ago

Here's an incredibly necessary and common example:

class Array {
  #arrayData = [];

  static isArray(obj) {
    if (!obj) { return; }
    try {
      return !!obj.#arrayData;
    } catch {
      return false;
    }
  }
}

Here's another:

let i;
class Foo {
  #id = i++;

  compare(other) {
    return other && this.#id === other.#id;
  }
}
babakness commented 6 years ago

function getFooClass () {
  let internalParameter = 'bookkeeping'
  let totalMembers = 0
  let prototype
  const Foo = function Foo() {
    prototype.totalMembers = ++totalMembers
    Object.defineProperty(this, 'totalMembers', {
      get: function() { return totalMembers },
      set: function() { throw 'read only' }
    });
  }
  prototype = Foo.prototype
  return Foo
}

const Foo = getFooClass()
const a = new Foo()
const b = new Foo()

a.totalMembers
// 2
b.totalMembers
// 2
Foo.prototype.totalMembers
// 2
a.totalMembers = 3
// read only

Is this roughly what you are communicating? Please clarify.

Note that internalParameter is not accessible at all from the outside and totalMembers is available from the outside to read only, but it can be changed from the inside. Also the entire thing about making Foo.prototype.totalMembers accessible can also be removed, obviously, so its just

function getFooClass () {
  let internalParameter = 'bookkeeping'
  let totalMembers = 0
  let prototype
  class Foo {
    constructor(){
      ++totalMembers
      Object.defineProperty(this, 'totalMembers', {
        get: function() { return totalMembers },
        set: function() { throw 'read only' }
      });  
    }
  }
  prototype = Foo.prototype
  return Foo
}

const Foo = getFooClass()
a = new Foo()
b = new Foo()

... or it can be made available to Foo but read only. Or not at all readable. Or we can return from getFooClass both the Foo class (prototype really) and a utility object that gives us stats, etc.

ljharb commented 6 years ago

Your "totalMembers" example is what would be a static private; my examples use a static method to read instance private fields off of multiple objects.

babakness commented 6 years ago

The above with the additional constraint that it would not be intended as an API... is that correct? I'm asking so I can clarify the problem to myself

ljharb commented 6 years ago

I'm not sure what you mean by "intended as an API"?

babakness commented 6 years ago

OK, I better understand now, thanks--I'll think it over for a while because there needs to be something better than wasting # for this purpose... not everyone even likes the class pattern let alone wants forfeit other potential utility of the symbol in the future.

Seriously if we are at a point where all other symbols are exhausted, extinguishing the last of the symbols from this endangered species is unwise. At least the # should be used where all programming styles would benefit. Consider for example the proposed |> operator.

https://github.com/tc39/proposal-pipeline-operator

Its great and all breed of programmers benefit from it. From different skill levels to different philosophies. And that's two characters while far more useful for day-to-day programming.

For now however, shouldn't there be a let or const declared to state if this can or can't be re-assigned? Seems more consistent.

ljharb commented 6 years ago

It’s a field, not a variable.

babakness commented 6 years ago

Here you go

const privateStuff = {}
const initPrivateStuff = function(instance) {
  privateStuff[instance.id] = {
    data: []
  }
}

class Foo {
  static isFoo(obj){
    if(obj && obj.id){
      return privateStuff[obj.id] != null ? true : false
    } else {
      return false
    }
  }
  constructor(){
    this.id = Symbol()
    initPrivateStuff(this)
  }
}

const foo = new Foo()
const bar = new Foo()

console.log(Foo.isFoo(bar))
//true
console.log(Foo.isFoo(foo))
//true

If I understand the issue correctly, please consider if your objections are more valuable than forfeiting # from scenarios that better benefit the community along with all other sound objections again # (lack of readability, inconsistency with the actual language, teachability, cryptic, etc.)

ljharb commented 6 years ago

(To be clear: # would be unacceptable for me in pipeline even if nothing else was using it)

And yes, i think instance private is so massively important that even if it killed every future syntax proposal forever, it would be worth it. Just to be clear. (I personally find # here quite readable; there’s nothing like this in the language to maintain consistency with; i think it’s trivially teachable; etc)

babakness commented 6 years ago

(To be clear: # would be unacceptable for me in pipeline even if nothing else was using it)

Disagree, but # would be a stupid syntax for that too.

And yes, i think instance private is so massively important that even if it killed every future syntax proposal forever, it would be worth it.

Given the remark about the future, fervor here is so high as to be unreasonable and pertinaciously obstinate.

Just to be clear. (I personally find # here quite readable; there’s nothing like this in the language to maintain consistency with; i think it’s trivially teachable; etc)

Strongly disagree.

What problems does # solve here not easily address by my (possible more portable) example? That said it is valid to make the language more ergonomic with measure cost benefits.

I personally feel JavaScript should grow its functional abilities. Its fine for classes to get better too, why not? People who don't like it can not use it. Done.

But I a strongly disagree that this pattern is so important as to deserve the only remaining symbol. No way.

https://stackoverflow.com/questions/2078978/functional-programming-vs-object-oriented-programming

https://blog.learningtree.com/functional-programming-object-orientated-programming/

PLEASE DO NOT STEAL #

Not for an offshoot feature in the OO pattern some might choose to avoid. No.

babakness commented 6 years ago

Using the example provided above

class Array {
  #arrayData = [];
  #example = 'test'
  static isArray(obj) {
    if (!obj) { return; }
    try {
      return !!obj.#arrayData;
    } catch {
      return false;
    }
  }
  method() {
    console.log(#arrayData)
    // mutation
    #arrayData.push(' :-( ')
    #example = 'something else' 
  }
}

Let's say there is some function private available within functions/class instantiated by new or Object.create that is a more elaborate production of the initPrivateStuff above. Private is essentially inside an implicit closure and has access to a specific instances private fields.

From here we have the syntax sugar such that private as a keyword de-sugars into the funciton


class Array {
  private arrayData = [] // de-sugars to private(this, 'arrayData', [])
  private example = 'test' // de-sugars to private(this, 'example', 'test')
  static isArray(obj) {
    if (!obj) { return; }
    try {
      return !! private(obj,'arrayData') // no sugar, however, for sugar `this` can be bound to something else
    } catch {
      return false;
    }
  }

  method() {
    console.log(private arrayData) // de-sugars to private( this,'arrayData' )
    private arrayData.push('  :-D ') // de-sugars to private( this,'arrayData' ).push(' :-D ')
    private example = 'something else' // de-sugars to private( this,'example', 'something else' )
  }
}

Or something along these lines... private is not available where private is already defined. Just like anything new in ES6. For example, Array.prototype.includes could already be overwritten by some library, and that's fine. There are versions of old JavaScript where super is not a reserved keyword.

Wouldn't this address the aforementioned concerns? It is also explicit and readable using words rather than any strange syntax using #.

In other words, # is replaced by a function private which has syntax sugar to be expressed using keyword where appropriate.

babakness commented 6 years ago

Added benefits to using private and the private keyword:

  1. Portability.
    
    // magic-library.js
    export const external = function(privateFn){
    privateFn(this, ... cool stuff.. )
    ... // more cool stuff
    return whatever
    }

// foo.js

import {external} from 'magic-library'

class Foo { method() { return external.call(this,private) } }



2. Readability. The keyword `private` is pretty self explanatory.
3. Future proof. JavaScript doesn't sacrifice `#` for this philosophy of OO
4. Teachable. Understanding `this` is important in JS. The concept of a `private` function and they sugar around `private` is as easy to understand once you understand why the `super` is there only within a constructor. Likewise `private` is there for classes and you can reproduce the behavior with `prototype` as aforementioned.
doodadjs commented 6 years ago

@babakness Your opinion against OO has nothing to do with that proposal. And FP is surely not the only way you go to develop a full computer software. But, like you, I'm on favor to use a keyword for the private scope instead of "#".

babakness commented 6 years ago

@doodadjs You're misreading. I use OO as well. However, anyone who would benefit from a better use of # has an opinion made relevant to this proposal by its use of #. Anyway, I read from your comment that you like OO a lot and still don't like # as used here. Good! 🤙

I'm offering help in straightening this out, so please first review this and see if it covers the use case where this proposal wants to take away #

https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361021019

If so, see how this alternate syntax might be suited or offer insight in making it work.

https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361033139

What do think?

I feel that some of those who want # used here are not trying to constructively solve the issue and just want to derail everything to get # used for this purpose. I appreciate your constructive help.

ljharb commented 6 years ago

It’s not “taking away” a token, it’s using one. “There might be a proposal in the future” is not a good reason to hamstring an existing proposal.

Do you have a concrete suggestion for a better use of #?

doodadjs commented 6 years ago

@babakness, @ljharb I really prefer the following structure :

class Foo {
  private user = 'doodadjs', // secret
  private password= 'doodadjs2018', // secret
  getList() {
    return window.fetch(`https://api.mycomain.com/foo/list?user=${this.user}&password=${this.password}`);
  }
}

const o = new Foo();
console.log(o.user); // access denied
console.log(o.password); // access denied

But because "o" is just an object with "proto" set to Foo.prototype, I'm thinking that it should still be possible to do :

o.user = 'mario';
o.password = 'mariobros';

To prevent this, may I suggest that the author of 'Foo' should do something like :

class Foo {
  @writable(false), @configurable(false)
  private user = 'doodadjs', // secret
  @writable(false), @configurable(false)
  private password= 'doodadjs2018', // secret
  getList() {
    return window.fetch(`https://api.mycomain.com/foo/list?user=${this.user}&password=${this.password}`);
  }
}
babakness commented 6 years ago

@ljharb Do I have a concrete suggestion for # today, no. Does this proposal have a good use for it? No.

Yes it is perfectly valid to not allow JS to be disqualified from the benefits of having a highly accessible # in place.

I've made a suggestion above, you not address that. You're not being constructive. There is this fixation on using#, not sure why. What's wrong with the suggestion above? private makes so much more sense and I've even suggested a way of accessing private field on other objects.

babakness commented 6 years ago

@doodadjs In case I missed it, how do you access private fields on other objects? See @ljharb example and my suggestion as well.

doodadjs commented 6 years ago

@babakness Private fields are private to the methods in their definition(1). I did an implementation of PRIVATE in doodad-js.

(1) "definition" being Foo and Foo.prototype combined.

(Sorry if I use the wrong terms)

EDIT: No... (1) should be : the class definition, sorry

bakkot commented 6 years ago

@babakness re: private arrayData for access: variations on this syntax have been raised and rejected several times before, including at least once or twice in the main syntax discussion thread and I think once or twice in other random threads. I don't have it in me to track them all down right now, but among other concerns, it doesn't play well with nested property access (e.g. what's the equivalent of this.#next.#data.type?).

To be clear, we've discussed this and alternative syntaxes at extreme length over several years, and the more general discussion of private fields in JS goes back to at least 1999. We are aware that some members of the community find this syntax distasteful, but we can't constantly keep rehashing the same variations on the same several dozen alternative syntax proposals.

doodadjs commented 6 years ago

@bakkot That's not just distasteful for me, but has a non-sense... Why scoping with special characters like '#' instead of a keyword like 'private'. That's weird, no ?

doodadjs commented 6 years ago

@bakkot What did you reserved for "protected" and "friend" and others ?

bakkot commented 6 years ago

@doodadjs The language reserved a number of keywords a long time ago, well before we had an idea of how or whether we'd use them, because we thought we might later use them for features which had not yet been designed. package is similarly reserved, but the ultimate syntax for the module system did not end up using that keyword. It happens.

littledan commented 6 years ago

We've discussed these issues extensively on other threads. Using the proposed syntax isn't what one might initially imagine, but it's a carefully considered trade-off that TC39 is willing to take. Is there something different to discuss here?

doodadjs commented 6 years ago

@littledan Probably what I've suggested at comment #361110146, and better.

ljharb commented 6 years ago

@doodadjs that won’t work at all, because it would make even the existence of a private field automatically observable from outside the class code - which defeats the point of “private”.

doodadjs commented 6 years ago

@ljharb No, the point of "private" is don't make the data available to the outsiders. In no way "private" means to hide the existence of a field... Where did you get that idea ?

EDIT: Was missing "to the outsiders".

ljharb commented 6 years ago

Absolutely it does, and it’s one of the most important parts of this proposal. The addition, removal, or modification of any private field must be 100% unobservable from outside the class (not counting Function.toString, of course).

babakness commented 6 years ago

@littledan Please point me to those threads to review, this seems at this point an unwise decision.

babakness commented 6 years ago

Edit: I'm linking this comment. I've provided links to three other comments for quick reference

A simple (granted limited) way of emulating private for reference https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361021019

Conjured idea on how it might work, there might be better ideas for sure https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361033139

The pros of using a private function that is has syntax sugar as well https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361034452

What follows address the question how one might recreate the following obj.#left.#right in this comment here

https://github.com/tc39/proposal-private-fields/issues/14#issuecomment-323007685


@bakkot

I looked over that forum, thanks for linking it.

I noted a comment you made

The former is technically feasible but, to my eye, a lot uglier than #x.

Again, though, I think that both of

having private.x refer to a private field of this when inside of a class, and a public property of the variable private when outside, and having to write private(private(obj).left).right instead of obj.#left.#right would be fatal to anything like this proposal.

Looks to me like an opportunity to add a syntax to pass an object to a method call. Given the |> one could do this

obj ▹ private ▹ .left ▹ private ▹ .right

It is so clear here what is going on, an object is being passed to private, then the .left property is accessed and that in turn is passed to private to access .right. I know that we can't pass an object to a method in JavaScript just yet, but we should be able to. That would be powerful.

const allTheRights = listOfObjectsPrivateOrPublic.map( obj => obj ▹ .right)

If anything having #foobar is anti-pattern in that it makes this easy task hard.

If anything we should work to make the above syntax possible because of its broader implications. Solving a problem here being but one application.

I'd rather see # used as a powerful operator. One with broad implication. Something that will benefit everyone in every 5 to 10 lines of code that they write, once they learn to break down their problems correctly.

Its not just about taste but direction. Regarding taste however, I have noted that you, like me, do bring up taste as a driving force in favor or this syntax; why is that better grounds than the taste against?

Anyway, even without this futuristic syntax, still presuming the|> operator (Stage 1), first we define a simple function

const getProp = prop => obj => obj[prop]

Then do

private( obj ) .left ▹ private ▹ getProp('right')

To my eyes it looks better than # to solve a problem solved this way just fine:

https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361021019

Besides this:

https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361034452

Is better. It allows a clear path to share code (portable).

Keep in mind also that there are entire languages that don't even have private fields, they rely on convention (ie, Python and the double __ underscore naming convention) and people write extremely advanced machine learning AI that could go from zero to best chess player in history in just hours using elaborate libraries in languages.

It is an unproven opinion that private fields are of such major importance that we must simply accept the use of # for mere "yes but it looks good to my eye" meaning "I concede that this is not a rational decision but I just like it and that's that."

I'm not saying don't have private fields, by all mean. I'm just saying don't ruin it for everyone not inside this circle.

The clear reason against it is that once # is used it taints the scope, fluidity, and power that it may have in the future, and that is a real concern.

bakkot commented 6 years ago

@littledan I don't think so, especially given this is stage 3.


@babakness

It is so clear here what is going on

I disagree rather strongly.

I'd rather see # used as a powerful operator. One with broad implication. Something that will benefit everyone in every 5 to 10 lines of code that they write, once they learn to break down their problems correctly.

As we've said before, I think if you have a better use for # you should propose it. We can't indefinitely hold things back because someone might someday invent a better use for similar syntax.

But I'm not sure that JavaScript has room for such an operator. We're constrained by the language as it already exists; there's only so much we can tack on to it.

Keep in mind also that there are entire languages that don't even have private fields, they rely on convention (ie, Python and the double __ underscore naming convention) and people write extremely advanced machine learning AI that could go from zero to best chess player in history in just hours using elaborate libraries in languages.

We went to the moon on assembly. I do not find this a particularly persuasive argument.

It is an unproven opinion that private fields are of such major importance that we must simply accept the use of # for mere "yes but it looks good to my eye" meaning "I concede that this is not a rational decision but I just like it and that's that."

Absent science we're not really in a position to do, all language design is opinion. This doesn't mean we shouldn't ever do any language design.

babakness commented 6 years ago

It is so clear here what is going on

I disagree rather strongly.

I disagree with your disagree even more strongly

Playing Devils Advocate... One pet peeve of mine in JS is assigning a variable key on an object.

const foo = {
  [bar] : true
} 

Why not this

const foo = {
  #bar : true
}

Where # means use the variable bar here. Why not? It looks better, no?

Or maybe there is some contrived reason we can't have

obj ▹ .foo

So then why not use # for this

obj ▹ #foo

So we can pass object to methods of the object

Or what about treading statements as expressions

const foo = # if (bar) { true } else { false}

And so on. I totally disagree with the justification with # for private fields because you think it looks good but have no real reason to use.

BTW, the thumbs up thumbs down is childish. This is obviously a forum most looked at by people behind this proposal, hyped up about ramming this thing through, and I expect some degree of disagreement.

ljharb commented 6 years ago

To your first one: why not? Because computed property keys are for more than just variables; they support any expression. Also, no, it does not look better. The brackets make for a beautiful and elegant symmetry with normal bracket access on objects.

Treating statements as expressions is already a proposal; it’d be do { whatever }.

Emoji reactions are ways to indicate agreement or disagreement when words would just be noise; I’m sorry you think this widely used community convention on Github is childish.

Let’s try another tack. What single-character token, that’s currently a syntax error, and doesn’t already have a conflicting meaning in JS, would private fields use if not #? If that’s in fact the only one this important proposal can use, then that’s the good reason to use it.

doodadjs commented 6 years ago

Maybe

MyClass#member 

be a shorten for

MyClass.prototype.member

?

ljharb commented 6 years ago

@doodadjs although that’s a notational convention that’s often used for just that purpose, i don’t think that in practice people need to often refer to prototype properties, certainly not enough for it to be worth new syntax.

doodadjs commented 6 years ago

@ljharb But "private" is rarely used too, you should focus more on "protected".

bakkot commented 6 years ago

@doodadjs Even if that were true, which I dispute: you can build protected on top of private, but not the reverse. Generally we try to provide the necessary foundation for building on, rather than simply satisfying the most common case.

doodadjs commented 6 years ago

To GitHub: Please give at least 5 minutes before emailing :)

@bakkot: I'm sorry, but how do you build "protected" on top of "private" ? Do you have an example (or a link) ?

bakkot commented 6 years ago

@doodadjs Sure, here's one way.

babakness commented 6 years ago

@bakkot What is wrong with comments alluded to here

https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361132678

It looks better but also works better. foo.#bar.#silly prevents doing

const allTheRights = listOfObjectsPrivateOrPublic.map( getProp('right') )

And the private way is portable per comment here

https://github.com/tc39/proposal-class-fields/issues/77#issuecomment-361034452

Because you'd have to write logic to deal with this mess. private as suggested by forum you linked is far superior to the "hashtag" syntax proposed as demonstrated.

@ljharb Because one isn't needed. Please address above linked comment.

Suggesting a single key is to suggest we need a single key. We don't. Why not use ##. It looks just as funny as a single #, is just as bad as a design choice, but has the advantage of not obstructing the future.

badUseOfHashtag

lloydjatkinson commented 6 years ago

Still not sure why new keywords can't be added, such as TypeScript's private to indicate private members? Seems more sane than a #.

I read the FAQ and did not see a compelling argument for not adding new keywords. The vague mentions of backwards compatibility I don't feel hold much weight. New breaking changes have been added several times to the language.

A private keyword is no more or less "breaking" than the # "keyword", but with the added benefit of not being confused with a trend I've seen recently for documentation of libraries to document an object's public members with the # symbol.

Example: https://momentjs.com/docs/#/get-set/day/

Moment#date is for the date of the month, and Moment#day is for the day of the week.

Furthermore, it simply isn't consistent with other keywords in the language.

Bizarre, really.

bakkot commented 6 years ago

@lloydjatkinson I'm sorry the FAQ was did not sufficiently convey our reasoning. I don't really know how I could phrase it any more clearly, but you might find it useful to read some of the previous discussions on exactly this topic.

The vague mentions of backwards compatibility I don't feel hold much weight.

Well... we do.

babakness commented 6 years ago

Edit: Please see my next message

A camel is a horse designed by a committee.