Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Derived class access #12

Closed
sugendran opened this issue Jul 3, 2017 · 10 comments
Closed

Derived class access #12

sugendran opened this issue Jul 3, 2017 · 10 comments

Comments

@sugendran
Copy link

Firstly, I don't want to rehash the "private" keyword argument discussion as that is happening elsewhere.

One thing that is not clear to me is the interaction between a class and the parent class that it is derived from. Could we get some examples of how private/public fields work (or not work) in the scenarios below?

class Shape {  
    // private field not accessible to any class that extends this
    // in C# land I would make this private.
    #id;

    // private field accessible to any class that extends this
    // in C# land I would make this protected
    #origin;

    // public readonly field
    // in C# land I would use the readonly keyword
    sides;

    // public field
    tag;

    constructor (id, x, y) {
        this.#id = id;
        this.#origin = new Point(x, y);
        this.sides = 0;
        this.tag = null;
    }
    equals (other) {
        return other.#id === #id;
    }
}
class Rectangle extends Shape {
    constructor (id, x, y, width, height) {
        super(id, x, y);
        this.sides = 4;
    }

    tests () {
        expect(this.#id).to.beUndefined();
        expect(this.#origin).to.beDefined();
        expect(this.sides).to.be(4);
        expect(this.tag).to.be(null);
    }
}

var r = new Rectangle("abc", 1, 2, 3, 4);
r.tag = 1234;
@bakkot
Copy link
Contributor

bakkot commented Jul 3, 2017

There isn't really much interaction. You can't even attempt to access a private field outside of the class in which it's defined - it is a SyntaxError to try.

So:

class Base {
  #f;
}
class Derived extends Base {
  m() {
    this.#f; // SyntaxError: #f is not defined
  }
}

As in other languages, you can have a private field in a derived class of the same name as a private field in a base class, and it will "just work":

class Base {
  #f = 0;
  baseMethod() {
    console.log(this.#f);
  }  
}

class Derived extends Base {
  #f = 1;
  derivedMethod() {
    console.log(this.#f);
  }
}

const instance = new Derived();
instance.baseMethod(); // 0
instance.derivedMethod(); // 1

There is not, in this proposal, any first-class notion of "protected", or of "readonly".

You can use Symbols for something kind of like protected fields:

const p = Symbol();
class Base {
  [p] = 0;
}
class Derived extends Base {
  m() {
    console.log(this[p]); // 0
  }
}

though of course these are accessible to other code via Object.getOwnPropertySymbols.

For "readonly" fields, you may be able to use a decorator, or you can use a getter:

"use strict";
class A {
  get v() {
    return 1;
  }
}
(new A()).v = 2; // TypeError: cannot set property 'v' of [object Object]

@sugendran
Copy link
Author

Having worked in some very large codebases these features are something I'm keen on. I don't really care about access through getOwnPropertySymbols or iterating over private/public fields via some sort of reflection. What I do care about is how developers signal intent to each other.

The use of the symbols could certainly make protected fields a possibility, but it would be nice to have some syntactic sugar for it.

'readonly' is an interesting one. Yes we can do it with getters. In the C# sense readonly means that the value can only be set in the constructor or field initialiser. This is a great way to signal that this value is meant to be immutable.

I'm not sure if the following is possible, but I guess this is how we'd do readonly using decorators.

class A {
  @readonly v;
  constructor(n) {
     this.v = n;
  }
}

becomes

class A {
  #v;
  get v() {
    return #v;
  }
  constructor(n) {
    this.#v = n;
  }
}

@sugendran
Copy link
Author

I did try to come up with an example of how protected could work, but I don't really like it. Also the use of this[a] is really not intuitive.

// approach 1 - using an internal class with getters and setters to make the experience a little better
const a = Symbol();
// exported outside the library
class Base {
  [a] = 0;

  addVal(val) {
    this[a] = this[a] + val;
  }

  addVal2(val) {
    this[a] += val;
  }
}

// internal - not exported outside the library
class InternalBase {
  get a() { return this[a]; }
  set a(val) { this[a] = val; }
}

class Derived extends InternalBase {
  m(b) {
    this.a = this.a * b;
  }
}

@littledan
Copy link
Member

We discussed whether private fields should be accessible outside the class that way at length in tc39/proposal-private-fields#33 . The conclusion was to maintain a strong privacy boundary.

@sugendran
Copy link
Author

Yeah, I tried reading that thread originally and had to stop when it spiraled out of control. Just did my third pass of it.

Was there further/offline discussion of @wycats proposal around adding a "secret" property for decorators? (tc39/proposal-private-fields#33 (comment))

If the decorators supported the "secret" property then you could implement private/protected members using symbols and decorators. Would it not be simpler than adding a private field slot?

And if you didn't want to add the 'secret' option then we just need to make the symbol not iterable, and we could achieve the same outcome.

@littledan
Copy link
Member

There was some further offline discussion. My impression of the end result is that both cases seem to have some use, but there's definitely a lot of use for the secret case, and we ended up taking the side of secret as the default. Decorators' integration with private fields (and methods) to allow a non-secret decorator is a major goal of decorators/private/fields integration. Maybe @wycats could say more.

@sugendran
Copy link
Author

I see the addition of a 'secret' or 'hidden' property to the decorators negating the need for private fields. Something like:

function private(target, key, descriptor) {
  descriptor.secret = true;
  descriptor.enumerable = false;
}

const field = new Symbol()
class Foo {
   @private [field] = "123";
}

Then a protected field is just one where you have access to the Symbol.

@littledan
Copy link
Member

@sugendran It's unclear to me how this would work. Could you elaborate?

@sugendran
Copy link
Author

My understanding of the private fields proposal is that we want private fields but we need to satisfy that:

  1. A derived class should not unintentionally overwrite the property of a base class
  2. A derived class should be prevented from knowing/accessing private properties on the base class
  3. A private property should be hidden from getOwnPropertySymbols and getOwnPropertyNames

1 and 2 can be satisfied if the property is a Symbol. A derived class would only be able to access the property using the Symbol, so it has be a deliberate choice on the developer to allow that to happen.

3 can be satisfied if we say that setting 'secret' to true on the descriptor prevents it from being listed in getOwnPropertySymbols and getOwnPropertyNames

@littledan
Copy link
Member

"secret" symbols were rejected in the ES6 cycle, since it was unclear how to explain why they don't hit Proxy traps (and, if they don't hit Proxy traps, what are proxies really good for?). This proposal uses private names instead, which are not properties at all.

I'm closing this issue for now; feel free to reopen if there's a new idea for how to square that circle.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants