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

Class Fields (Stage 3) #12

Open
babel-bot opened this Issue Jul 10, 2017 · 42 comments

Comments

Projects
None yet
@babel-bot

babel-bot commented Jul 10, 2017

Original issue submitted by @babel-bot in babel/babel#4408

Champions: @jeffmo (public class fields) @littledan (private + combined)
Spec Repo: https://github.com/tc39/proposal-class-fields
Spec Text: https://tc39.github.io/proposal-class-fields/
Slides: https://drive.google.com/file/d/0B-TAClBGyqSxWHpyYmg2UnRHc28/view

Moved to Stage 3 at the July 2017 meeting: https://github.com/tc39/agendas/blob/master/2017/07.md (https://twitter.com/lbljeffmo/status/890679542007738368)

Examples

class C {
  static x = 1, #y, [a];
  z, #w = 2, [b];
  
  a() {
    this.z;
    this.#w++;
    #w; // #w is this.#w
  }
}

Parsing/ESTree

  • AST handled already in current classProperties and classPrivateProperties.
  • May need to rename though (new plugin name).
  • make sure no "private computed" parse
  • need to support comma separated properties.

Transform

  • combine the class-properties plugin + tbd private properties one

Contacts

@hzoo hzoo changed the title from new feature: Private Fields (T7508) to Class Fields (Stage 2) Jul 10, 2017

@hzoo hzoo added the Proposal Issue label Jul 10, 2017

@hzoo hzoo referenced this issue Jul 27, 2017

Closed

July 2017 #19

10 of 10 tasks complete

@hzoo hzoo changed the title from Class Fields (Stage 2) to Class Fields (Stage 3) Jul 28, 2017

@vjpr vjpr referenced this issue Nov 8, 2017

Open

Tooling implementation status #57

5 of 10 tasks complete
@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Mar 5, 2018

dunno how much of an help this is, but I've been experimenting already with this and my conclusion is that, to represent the current proposal, each class needs it's own WeakMap.

Example

source

class A {
  #value = 1;
  valueOf() {
    return this.#value;
  }
}

class B extends A {
  #value = 2;
  toString() {
    return String(this.#value);
  }
}

pseudo target

const privatesA = new WeakMap;
function A() {
  privatesA.set(this, {__proto__: null, value: 1});
}
Object.defineProperties(
  A.prototype,
  {
    valueOf: {
      configurable: true,
      writable: true,
      value: function () {
        return privatesA.get(this).value;
      }
    }
  }
);

const privatesB = new WeakMap;
function B() {
  A.call(this);
  privatesB.set(this, {__proto__: null, value: 2});
}
Object.defineProperties(
  Object.setPrototypeOf(
    Object.setPrototypeOf(B, A).prototype,
    A.prototype
  ),
  {
    constructor: {
      configurable: true,
      writable: true,
      value: B
    },
    toString: {
      configurable: true,
      writable: true,
      value: function () {
        return String(privatesB.get(this).value);
      }
    }
  }
);

in that way new B().valueOf() would be 1 and new B().toString() would be "2" + no internal property ever leaks through symbols.

WebReflection commented Mar 5, 2018

dunno how much of an help this is, but I've been experimenting already with this and my conclusion is that, to represent the current proposal, each class needs it's own WeakMap.

Example

source

class A {
  #value = 1;
  valueOf() {
    return this.#value;
  }
}

class B extends A {
  #value = 2;
  toString() {
    return String(this.#value);
  }
}

pseudo target

const privatesA = new WeakMap;
function A() {
  privatesA.set(this, {__proto__: null, value: 1});
}
Object.defineProperties(
  A.prototype,
  {
    valueOf: {
      configurable: true,
      writable: true,
      value: function () {
        return privatesA.get(this).value;
      }
    }
  }
);

const privatesB = new WeakMap;
function B() {
  A.call(this);
  privatesB.set(this, {__proto__: null, value: 2});
}
Object.defineProperties(
  Object.setPrototypeOf(
    Object.setPrototypeOf(B, A).prototype,
    A.prototype
  ),
  {
    constructor: {
      configurable: true,
      writable: true,
      value: B
    },
    toString: {
      configurable: true,
      writable: true,
      value: function () {
        return String(privatesB.get(this).value);
      }
    }
  }
);

in that way new B().valueOf() would be 1 and new B().toString() would be "2" + no internal property ever leaks through symbols.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Mar 5, 2018

Isn't that only the case if "value" is transformed to a string? I'd think you could use an in-scope constant - like a symbol or an object literal - and then it'd be unique and you could share one WeakMap for the entire file.

ljharb commented Mar 5, 2018

Isn't that only the case if "value" is transformed to a string? I'd think you could use an in-scope constant - like a symbol or an object literal - and then it'd be unique and you could share one WeakMap for the entire file.

@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Mar 5, 2018

I'm not sure I am following you ... the example has nothing to do with the toString or valueOf case, it's a proof of concept of the implementation details and nothing else: WeakMap is the answer to this problem. It scales, it's not the best for performance reasons, yet is the most reliable.

WebReflection commented Mar 5, 2018

I'm not sure I am following you ... the example has nothing to do with the toString or valueOf case, it's a proof of concept of the implementation details and nothing else: WeakMap is the answer to this problem. It scales, it's not the best for performance reasons, yet is the most reliable.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Mar 5, 2018

I understand that you must use a WeakMap. I'm responding to "each class needs it's own WeakMap" - it seems like both class A and B could share the same WeakMap, since each "value" is a distinct PrivateName.

ljharb commented Mar 5, 2018

I understand that you must use a WeakMap. I'm responding to "each class needs it's own WeakMap" - it seems like both class A and B could share the same WeakMap, since each "value" is a distinct PrivateName.

@piranna

This comment has been minimized.

Show comment
Hide comment
@piranna

piranna Mar 6, 2018

A single WeakMap could be used as @ljharb says, but I think using a WeakMap for each class simplify the implementation.

piranna commented Mar 6, 2018

A single WeakMap could be used as @ljharb says, but I think using a WeakMap for each class simplify the implementation.

@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Mar 6, 2018

It's not just a matter of implementation, it's a matter of standard. You cannot have shared WM between classes because private field #a accessed via an inherited method is NOT the same private field #a accessed by subclass method.

Please understand the current proposal before premature optimizations

WebReflection commented Mar 6, 2018

It's not just a matter of implementation, it's a matter of standard. You cannot have shared WM between classes because private field #a accessed via an inherited method is NOT the same private field #a accessed by subclass method.

Please understand the current proposal before premature optimizations

@littledan

This comment has been minimized.

Show comment
Hide comment
@littledan

littledan Mar 6, 2018

It might be better to discuss this on the implementation PR than on this thread. I like the implementation strategy of that patch: In spec-compliant mode, there's one WeakMap per field, and in loose mode, there are no WeakMaps, since it's all based on string properties. It's important to track which instances have each individual field, since an initializer can throw an exception and leak an object that has some fields but not others.

littledan commented Mar 6, 2018

It might be better to discuss this on the implementation PR than on this thread. I like the implementation strategy of that patch: In spec-compliant mode, there's one WeakMap per field, and in loose mode, there are no WeakMaps, since it's all based on string properties. It's important to track which instances have each individual field, since an initializer can throw an exception and leak an object that has some fields but not others.

@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Mar 6, 2018

@littledan agreed, but I couldn't literally find related changes. If there's already a PR I'll have a look there, thanks

WebReflection commented Mar 6, 2018

@littledan agreed, but I couldn't literally find related changes. If there's already a PR I'll have a look there, thanks

@hzoo

This comment has been minimized.

Show comment
Hide comment
@hzoo

hzoo Mar 12, 2018

Member

New private props PR babel/babel#6120

Member

hzoo commented Mar 12, 2018

New private props PR babel/babel#6120

@vjpr

This comment has been minimized.

Show comment
Hide comment
@vjpr

vjpr commented Mar 18, 2018

Replaced with babel/babel#7555

@hzoo hzoo referenced this issue Apr 21, 2018

Closed

Class Fields (combined) - Stage 2 #6692

1 of 5 tasks complete
@hzoo

This comment has been minimized.

Show comment
Hide comment
@hzoo

hzoo May 24, 2018

Member

Private fields shipped in https://github.com/babel/babel/releases/tag/v7.0.0-beta.48

TC39 is looking for feedback from the committee, since unlike public fields which has extensive usage/docs/videos/etc, private has not (even as Stage 3) since it hasn't shipped in Babel or other implementations until recently.

Member

hzoo commented May 24, 2018

Private fields shipped in https://github.com/babel/babel/releases/tag/v7.0.0-beta.48

TC39 is looking for feedback from the committee, since unlike public fields which has extensive usage/docs/videos/etc, private has not (even as Stage 3) since it hasn't shipped in Babel or other implementations until recently.

@mheiber

This comment has been minimized.

Show comment
Hide comment
@mheiber

mheiber Jun 20, 2018

The following seems to be allowed by the Babel parser:

class Foo {
    #    p = 0;
    constructor() {
        this.#   p   ++;
    }

    print() {
        console.log(this   .   #    p);
    }
}

However, it seems to violate the lexical grammar in the spec proposal:

PrivateName:: #IdentifierName

Is the intention that the transformation itself (not the parser) should disallow this.# foo and similar productions?

cc @ramin25 who helped find this, and @rricard, who is working on private fields.

mheiber commented Jun 20, 2018

The following seems to be allowed by the Babel parser:

class Foo {
    #    p = 0;
    constructor() {
        this.#   p   ++;
    }

    print() {
        console.log(this   .   #    p);
    }
}

However, it seems to violate the lexical grammar in the spec proposal:

PrivateName:: #IdentifierName

Is the intention that the transformation itself (not the parser) should disallow this.# foo and similar productions?

cc @ramin25 who helped find this, and @rricard, who is working on private fields.

@rricard

This comment has been minimized.

Show comment
Hide comment
@rricard

rricard Jun 20, 2018

I can try to take a look when I'm done with static private fields (I'm getting there, slowly but I'm getting there, and @tim-mc helps me as well on this one). This issue though seems to be a parser-level issue, I never worked directly on that and only consumed the AST but that might be an interesting thing to try to fix.

rricard commented Jun 20, 2018

I can try to take a look when I'm done with static private fields (I'm getting there, slowly but I'm getting there, and @tim-mc helps me as well on this one). This issue though seems to be a parser-level issue, I never worked directly on that and only consumed the AST but that might be an interesting thing to try to fix.

@nicolo-ribaudo

This comment has been minimized.

Show comment
Hide comment
@nicolo-ribaudo

nicolo-ribaudo Jun 20, 2018

Member

It should be disallowed by the parser 👍

Member

nicolo-ribaudo commented Jun 20, 2018

It should be disallowed by the parser 👍

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Aug 3, 2018

That sounds like a transform you could certainly build, but making compiler output human-readable at the cost of needing an additional runtime library doesn’t seem like a worthy tradeoff.

ljharb commented Aug 3, 2018

That sounds like a transform you could certainly build, but making compiler output human-readable at the cost of needing an additional runtime library doesn’t seem like a worthy tradeoff.

@piranna

This comment has been minimized.

Show comment
Hide comment
@piranna

piranna Aug 3, 2018

Maybe some parts or ideas could be integrated? I think the specific code for ES5 could be removed and left only the private and protected parts, and left Babel to adapt it itself...

By the way @trusktr, can you provide an example of protected attributes and methods? Examples seems to show only private ones... And are protecte attributes in any standard?

piranna commented Aug 3, 2018

Maybe some parts or ideas could be integrated? I think the specific code for ES5 could be removed and left only the private and protected parts, and left Babel to adapt it itself...

By the way @trusktr, can you provide an example of protected attributes and methods? Examples seems to show only private ones... And are protecte attributes in any standard?

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

additional runtime library

@ljharb If lowclass (or similar idea) gets adopted into Babel, then it won't be "additional". :D

an example of protected attributes and methods?

@piranna Sure, there's many examples in the tests. For example, search for "protected" in this file.

Small example:

// Parent.js
import Class from 'lowclass'

export default Class('Parent', {
	protected: {
		parentLog() {
			console.log( 'Parent!' )
		}
	}
})
// Child.js
import Parent from './Parent'

export default Parent.subclass('Child', ({Protected}) => ({
	childLog() {
		Protected(this).parentLog()
		console.log( 'Child!' )
	},
}))
// app.js
import Child from './Child'

const o = new Child

o.childLog() // it works

// output:
// Parent!
// Child!

o.parentLog() // ERROR, undefined is not a function (because it is not public)

trusktr commented Aug 3, 2018

additional runtime library

@ljharb If lowclass (or similar idea) gets adopted into Babel, then it won't be "additional". :D

an example of protected attributes and methods?

@piranna Sure, there's many examples in the tests. For example, search for "protected" in this file.

Small example:

// Parent.js
import Class from 'lowclass'

export default Class('Parent', {
	protected: {
		parentLog() {
			console.log( 'Parent!' )
		}
	}
})
// Child.js
import Parent from './Parent'

export default Parent.subclass('Child', ({Protected}) => ({
	childLog() {
		Protected(this).parentLog()
		console.log( 'Child!' )
	},
}))
// app.js
import Child from './Child'

const o = new Child

o.childLog() // it works

// output:
// Parent!
// Child!

o.parentLog() // ERROR, undefined is not a function (because it is not public)
@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

  • The rule for lowclass "protected" properties is they are accessible in any subclass (and by super classes), but not public. It's similar to C++ and other languages.
  • "Private" properties are only accessible by the owning class (similar to C++ and other languages).
  • Anyone can access public.
  • The access helpers can be leaked out of the class (as in one of my above examples) to do things similar to "package protected" in Java. (Examples in the README)
  • private and protected fields can be shadowed by subclasses (unlike other languages that instead throw a compile error), so this is inline with this class fields proposal.
  • besides shadowing, lowclass also offers "private inheritance"

trusktr commented Aug 3, 2018

  • The rule for lowclass "protected" properties is they are accessible in any subclass (and by super classes), but not public. It's similar to C++ and other languages.
  • "Private" properties are only accessible by the owning class (similar to C++ and other languages).
  • Anyone can access public.
  • The access helpers can be leaked out of the class (as in one of my above examples) to do things similar to "package protected" in Java. (Examples in the README)
  • private and protected fields can be shadowed by subclasses (unlike other languages that instead throw a compile error), so this is inline with this class fields proposal.
  • besides shadowing, lowclass also offers "private inheritance"
@littledan

This comment has been minimized.

Show comment
Hide comment
@littledan

littledan Aug 3, 2018

@trusktr I think the current style of output from Babel is good. I don't think "readable" output is a sustainable goal for Babel transforms for TC39 proposals, given that the language semantics are pretty subtle. If you want to propose particular semantics, I think that's better done in TC39 than in this repository; see CONTRIBUTING.md.

littledan commented Aug 3, 2018

@trusktr I think the current style of output from Babel is good. I don't think "readable" output is a sustainable goal for Babel transforms for TC39 proposals, given that the language semantics are pretty subtle. If you want to propose particular semantics, I think that's better done in TC39 than in this repository; see CONTRIBUTING.md.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Aug 3, 2018

I also don’t think it would be appropriate for Babel to contain any concept of “protected”, because the language has none - and it in my opinion is not likely to ever have one.

ljharb commented Aug 3, 2018

I also don’t think it would be appropriate for Babel to contain any concept of “protected”, because the language has none - and it in my opinion is not likely to ever have one.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

@littledan Readability would only be a bonus. I'm just showing a runtime implementation that has all the features needed for everything in this proposal (and it can be tweaked). Maybe it'll can give someone ideas.

don’t think it would be appropriate for Babel to contain any concept of “protected”, because the language has none

Not sure what you mean. JS has a protected reserved keyword, just like private. How is it more appropriate for Babel to contain a "private" implementation than it is for "protected"? Is it because "private" is spec'd in the class fields proposal, and "protected" is not?

We can easily add it.


When I write classes, a large amount of the time I want "protected" (and I use lowclass for it), especially when designing an API that someone will interact with that is composed of a class hierarchy where I don't want the end user to reach into protected parts of instances, but I do want my subclasses to access protected parts. I like that I can guarantee a certain public interface to end users of my instances while keeping protected properties hidden.

I made a poll to see if people want protected (I hope some people will vote!): https://twitter.com/trusktr/status/1025466478626136064

trusktr commented Aug 3, 2018

@littledan Readability would only be a bonus. I'm just showing a runtime implementation that has all the features needed for everything in this proposal (and it can be tweaked). Maybe it'll can give someone ideas.

don’t think it would be appropriate for Babel to contain any concept of “protected”, because the language has none

Not sure what you mean. JS has a protected reserved keyword, just like private. How is it more appropriate for Babel to contain a "private" implementation than it is for "protected"? Is it because "private" is spec'd in the class fields proposal, and "protected" is not?

We can easily add it.


When I write classes, a large amount of the time I want "protected" (and I use lowclass for it), especially when designing an API that someone will interact with that is composed of a class hierarchy where I don't want the end user to reach into protected parts of instances, but I do want my subclasses to access protected parts. I like that I can guarantee a certain public interface to end users of my instances while keeping protected properties hidden.

I made a poll to see if people want protected (I hope some people will vote!): https://twitter.com/trusktr/status/1025466478626136064

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

By the way, it's possible to add encapsulation with lowclass around existing classes. (also in the README)

This shows that such an implementation can exist on top of Babel's class transform output as is:

import protect from 'lowclass' // call it "protect" instead of "Class"

const Thing = protect( ({ Private }) => {

    return class Thing {

        constructor() {
            // make the property truly private
            Private(this).privateProperty = "yoohoo"
        }

        someMethod() {
            console.log('Private value is:', Private(this).privateProperty)
        }

    }

})

const t = new Thing
t.someMethod() // "Private value is: yoohoo"

The class definer function can return any class, for example one created with Babel helpers.

Something like a babel-plugin-transform-class-fields could easily wrap the output from @babel/plugin-transform-classes. I think I'd like to give it a shot! It'd be nice to have class + protected.

trusktr commented Aug 3, 2018

By the way, it's possible to add encapsulation with lowclass around existing classes. (also in the README)

This shows that such an implementation can exist on top of Babel's class transform output as is:

import protect from 'lowclass' // call it "protect" instead of "Class"

const Thing = protect( ({ Private }) => {

    return class Thing {

        constructor() {
            // make the property truly private
            Private(this).privateProperty = "yoohoo"
        }

        someMethod() {
            console.log('Private value is:', Private(this).privateProperty)
        }

    }

})

const t = new Thing
t.someMethod() // "Private value is: yoohoo"

The class definer function can return any class, for example one created with Babel helpers.

Something like a babel-plugin-transform-class-fields could easily wrap the output from @babel/plugin-transform-classes. I think I'd like to give it a shot! It'd be nice to have class + protected.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Aug 3, 2018

@trusktr yes, and the private fields proposal intentionally and explicitly chose not to address "protected", and https://github.com/tc39/proposal-private-fields/blob/e704f531c33795ca34ede86e1c78e87798e00064/DECORATORS.md#protected-style-state-through-decorators-on-private-state shows how you can achieve protected without special syntax for it.

ljharb commented Aug 3, 2018

@trusktr yes, and the private fields proposal intentionally and explicitly chose not to address "protected", and https://github.com/tc39/proposal-private-fields/blob/e704f531c33795ca34ede86e1c78e87798e00064/DECORATORS.md#protected-style-state-through-decorators-on-private-state shows how you can achieve protected without special syntax for it.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

I found a couple issues with the current implementation: babel/babel#8421.

trusktr commented Aug 3, 2018

I found a couple issues with the current implementation: babel/babel#8421.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

Interestingly, the problems described there are the same as in other languages.

That version of "protected" is exactly the same as implementing "private" with a weakmap, then assigning the WeakMap onto each instance. Not very protected.

There's valid cases for "protected" where protected members can not be public: An instance factory can provide instances, while the class hierarchy is encapsulated. Therefore, all the "protected" members are still hidden by design, and library maintainers can benefit from the protected pattern.

Simply making everything public (like in the decorator example) isn't the same.

trusktr commented Aug 3, 2018

Interestingly, the problems described there are the same as in other languages.

That version of "protected" is exactly the same as implementing "private" with a weakmap, then assigning the WeakMap onto each instance. Not very protected.

There's valid cases for "protected" where protected members can not be public: An instance factory can provide instances, while the class hierarchy is encapsulated. Therefore, all the "protected" members are still hidden by design, and library maintainers can benefit from the protected pattern.

Simply making everything public (like in the decorator example) isn't the same.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Aug 3, 2018

Since JS has runtime subclassing, anything that's "protected" is in practice fully public.

ljharb commented Aug 3, 2018

Since JS has runtime subclassing, anything that's "protected" is in practice fully public.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 3, 2018

Oh yeah, I almost forgot about access to the prototype. A way to defend against that would be to have a library's leaf-most classes define a private property, leak the access helper inside the module scope (f.e. with lowclass), and let library code check private values by reference to prevent duck-typing of any sort.

trusktr commented Aug 3, 2018

Oh yeah, I almost forgot about access to the prototype. A way to defend against that would be to have a library's leaf-most classes define a private property, leak the access helper inside the module scope (f.e. with lowclass), and let library code check private values by reference to prevent duck-typing of any sort.

@ljharb

This comment has been minimized.

Show comment
Hide comment
@ljharb

ljharb Aug 3, 2018

I can't conceive of any way to do that that allows for subclassing at any time, even later, without risking exposing protected things to the world. I believe that it's truly impossible in JS to do this, which is why I think that "protected" is a wholly inappropriate idiom for the language.

ljharb commented Aug 3, 2018

I can't conceive of any way to do that that allows for subclassing at any time, even later, without risking exposing protected things to the world. I believe that it's truly impossible in JS to do this, which is why I think that "protected" is a wholly inappropriate idiom for the language.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 7, 2018

That's what I meant, you don't allow further subclassing. We could also introduce a final keyword, to prevent subclassing.

I believe that it's truly impossible in JS to do this

@bmeck shared with both of us the "gateway" pattern, which shows it is possible to (for example) throw an error for invalid imports.

A simple implementation would be to track a counter inside a module scope which desires to have a "private" export, and increment the counter for every import of the private export. As a library author, we will know how many times the counter should be incremented. Once the counter goes beyond that number, we throw an error and leave the lib in a broken useless state.

The person trying to use the library has no choice but to fork the library source code and change the code in order to import and extend the desired class. This is the same with any other language: if you have the source, you can do whatever you want to it.

With this technique, we can export only a "final" class from a lib. This of course has implication on the library structure and implementation and how the user imports the export; that's the inconvenient part. But it's possible. We can abstract away some of the complexities involved with the gateway pattern for a specific use case.

After thinking about this for a while, I think protected is still valid, and it is not the same as the fake still-public technique that the class fields proposal suggests using (this.protected.foo).

trusktr commented Aug 7, 2018

That's what I meant, you don't allow further subclassing. We could also introduce a final keyword, to prevent subclassing.

I believe that it's truly impossible in JS to do this

@bmeck shared with both of us the "gateway" pattern, which shows it is possible to (for example) throw an error for invalid imports.

A simple implementation would be to track a counter inside a module scope which desires to have a "private" export, and increment the counter for every import of the private export. As a library author, we will know how many times the counter should be incremented. Once the counter goes beyond that number, we throw an error and leave the lib in a broken useless state.

The person trying to use the library has no choice but to fork the library source code and change the code in order to import and extend the desired class. This is the same with any other language: if you have the source, you can do whatever you want to it.

With this technique, we can export only a "final" class from a lib. This of course has implication on the library structure and implementation and how the user imports the export; that's the inconvenient part. But it's possible. We can abstract away some of the complexities involved with the gateway pattern for a specific use case.

After thinking about this for a while, I think protected is still valid, and it is not the same as the fake still-public technique that the class fields proposal suggests using (this.protected.foo).

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 7, 2018

After using the gateway pattern, implementing the "final" class is easy, in a way that works in most cases:

class FinalClass extends PrivateClass {
  constructor() {
    if (!this.__proto__ === FinalClass.prototype) selfDestruct()
  }
}

Well, anyways, if the language doesn't gain protected, I have my own version of it. :)

trusktr commented Aug 7, 2018

After using the gateway pattern, implementing the "final" class is easy, in a way that works in most cases:

class FinalClass extends PrivateClass {
  constructor() {
    if (!this.__proto__ === FinalClass.prototype) selfDestruct()
  }
}

Well, anyways, if the language doesn't gain protected, I have my own version of it. :)

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Aug 7, 2018

isiahmeadows commented Aug 7, 2018

@littledan

This comment has been minimized.

Show comment
Hide comment
@littledan

littledan Aug 8, 2018

Please, I'd encourage you to have discussions about proposal semantics within TC39, rather than in the Babel project.

littledan commented Aug 8, 2018

Please, I'd encourage you to have discussions about proposal semantics within TC39, rather than in the Babel project.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 8, 2018

just check that new.target === FinalClass

Sweeet. So very possible to make use of a real protected, even if not completely convenient. But a library author can take the hit for the benefit of many others.

I'd encourage you to have discussions about proposal semantics within TC39

👍

trusktr commented Aug 8, 2018

just check that new.target === FinalClass

Sweeet. So very possible to make use of a real protected, even if not completely convenient. But a library author can take the hit for the benefit of many others.

I'd encourage you to have discussions about proposal semantics within TC39

👍

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Aug 8, 2018

@trusktr That's for "final class" within constructors, not "protected" methods. 😉

isiahmeadows commented Aug 8, 2018

@trusktr That's for "final class" within constructors, not "protected" methods. 😉

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 9, 2018

@isiahmeadows Yes, and if a library exports a "final" class, then this prevents end users from exposing protected members through class extension (they can't extend the final class).

trusktr commented Aug 9, 2018

@isiahmeadows Yes, and if a library exports a "final" class, then this prevents end users from exposing protected members through class extension (they can't extend the final class).

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 9, 2018

So therefore, protected members are valid, and they are definitely not the same as a public this.protected namespace.

trusktr commented Aug 9, 2018

So therefore, protected members are valid, and they are definitely not the same as a public this.protected namespace.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Aug 9, 2018

@trusktr It doesn't protect methods, just the constructor. The best you can do is Object.freeze(FinalClass.prototype), but you can't otherwise protect them - objects aren't branded unforgeably with their class.

isiahmeadows commented Aug 9, 2018

@trusktr It doesn't protect methods, just the constructor. The best you can do is Object.freeze(FinalClass.prototype), but you can't otherwise protect them - objects aren't branded unforgeably with their class.

@isiahmeadows

This comment has been minimized.

Show comment
Hide comment
@isiahmeadows

isiahmeadows Aug 9, 2018

But either way, this is getting off-topic, so if that doesn't address your concerns, feel free to raise it on es-discuss.

isiahmeadows commented Aug 9, 2018

But either way, this is getting off-topic, so if that doesn't address your concerns, feel free to raise it on es-discuss.

@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Aug 18, 2018

The protected members in my lowclass implementation are not exposed on FinalClass.prototype (otherwise they would be public). The only way to expose them is through class inheritance, just like in other languages.

EDIT: Oh, I see what you mean, that the prototype can still be extended without the original constructor. In this case, at least with lowclass, the extender still can not use protected or private members directly. Existing public methods can still access protected/private methods though (but new public methods can't).

My implementation can still make it work: the original constructor can keep track of instances in a module-scoped WeakMap, and the user's custom constructor can't. Then calling any methods can throw as useful message like "you can't extend this class. :P".

TLDR It's doable, I can see the path to making it work. I might just add this to lowclass...

A language final would be sugar (but better, because no restrictions by the language in the implementation).

Alright, will continue in ESDiscuss.

trusktr commented Aug 18, 2018

The protected members in my lowclass implementation are not exposed on FinalClass.prototype (otherwise they would be public). The only way to expose them is through class inheritance, just like in other languages.

EDIT: Oh, I see what you mean, that the prototype can still be extended without the original constructor. In this case, at least with lowclass, the extender still can not use protected or private members directly. Existing public methods can still access protected/private methods though (but new public methods can't).

My implementation can still make it work: the original constructor can keep track of instances in a module-scoped WeakMap, and the user's custom constructor can't. Then calling any methods can throw as useful message like "you can't extend this class. :P".

TLDR It's doable, I can see the path to making it work. I might just add this to lowclass...

A language final would be sugar (but better, because no restrictions by the language in the implementation).

Alright, will continue in ESDiscuss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment