Skip to content

RFC for private class fields - #241

Closed
skberkeley wants to merge 20 commits into
masterfrom
skberkeley/private-fields
Closed

RFC for private class fields#241
skberkeley wants to merge 20 commits into
masterfrom
skberkeley/private-fields

Conversation

@skberkeley

Copy link
Copy Markdown
Member

Rendered

Enjoy!

@skberkeley skberkeley self-assigned this Aug 11, 2026
@InfraredGodYT

Copy link
Copy Markdown

Considering public exists, not having a private keyword makes very little sense. I have yet to find a good explanation to justify this pattern:

class Point  
	public x: number  
	public y: number  
	local #cachedLength: number?  
  
       -- etc
end  

@Bottersnike

Bottersnike commented Aug 11, 2026

Copy link
Copy Markdown

This design doesn't work in conjunction with inheritance. If we consider the example of

open class Base
    local #foo: number

    function #__init(self)
        self.foo = 1
    end
    function getBaseFoo(self)
        return self.#foo
    end
end
class Sub extends Base
    local #foo: number
    function __init(self)
        self.#foo = 2
    end
    function getBothFoos(self)
        return self.#foo, self:getBaseFoo()
    end
end

getBothFoos is indirectly accessing Base.#foo through the inherited getter, but there's no way it can ever be initialised. Sub has no way to ever call Base's constructor to initialise that value.

NB I know #__init is explicitly not supported right now, but it's something worth considering during the general design of privates because it's hard to retcon the design, and we could similarly imagine Base having a method defined as

protected function setBaseFoo(self, val)
    self.#foo = val
end

to allow subclasses to control foo but otherwise setBaseFoo is considered private (for encapsulation purposes). With # meaning "private" it's hard to then add protected in a sensible way.

@TenebrisNoctua

TenebrisNoctua commented Aug 11, 2026

Copy link
Copy Markdown

Let's talk about the syntax.

The "local" and "#" being used to declare public or private fields is completely awful. The RFC proposes them as an alternative to a potential ambiguity issue with actual access specifiers.

First of all, what does even "local" mean in the context of classes? Are they local variables? Or fields? Are they the same as our normal local? Will the user be able to differentiate between them?

And the "#" symbol used for private fields is horrible, especially combined with the "local", it looks like I'm trying to get the length of a variable, rather than declaring a private field.

This syntax is confusing, hard to read, and provides little to no benefit other than resolving an ambiguity issue, which I believe could be resolved with other means.

That aside, the problem of private constructors becoming difficult to implement have arisen immediately after reserving .new(), just like I guessed. It is mind boggling that the team still wishes to keep it when in reality it will give them a hard time implementing features.

@gaymeowing

Copy link
Copy Markdown
Contributor

Using the length operator for identifying a field as private is for lack of a better word "crazy". Especially there is a public keyword, so it'd just make sense to have a private keyword.
This will be just as confusing as learning how metatables work for beginners, when one of the goals of classes is to make writing class code more accessible I'm hopefully assuming.

Comment thread docs/classes-private-fields.md Outdated
end

function getMagnitude(self)
return self:#computeMagnitude()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think :#method() looks grotesque and almost serves as a drawback to this counterexample on its own. consider:

return self:
    #computeMagnitude

even without whitespace, it looks far too similar to e.g. #arr, and feels wildly inconsistent. i'm sympathetic to the issues which might come from keywords but i feel like this syntax needs some work

Comment thread docs/classes-private-fields.md Outdated
local a = Base.new(...)
local b = Derived.new(...)

-- Should this compare Base.x to Base.x, or Base.x to Derived.x?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this even be allowed? i feel like having two values for x in and of itself is a pretty large footgun, even with an access modifier for the private one

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the equivalent of this is legal in other languages. If we were to raise some sort of error on public x in Derived shadowing private x in Base, this worsens the fragile base class problem where the author of Base is now restricted in what private property names they can use without causing errors downstream.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm definitely sympathetic to fragile base class problems, but I'm not sure the end justifies the means here. Is it really a solution if the result which we're making possible is confusing to read anyways? Admittedly it could just be a lint, but I'd personally prefer that kind of ambiguity just yell at people. Rename symbol exists for stuff like this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the equivalent of this is legal in other languages. If we were to raise some sort of error on public x in Derived shadowing private x in Base, this worsens the fragile base class problem where the author of Base is now restricted in what private property names they can use without causing errors downstream.

The fragile Base class problem is an issue that you mostly cannot resolve on your own. I would prefer a ban rather than having a solution which is incredibly hard to work with.

@skberkeley

Copy link
Copy Markdown
Member Author

Let's talk about the syntax.

The "local" and "#" being used to declare public or private fields is completely awful. The RFC proposes them as an alternative to a potential performance issue with actual access specifiers.

First of all, what does even "local" mean in the context of classes? Are they local variables? Or fields? Are they the same as our normal local? Will the user be able to differentiate between them?

And the "#" symbol used for private fields is horrible, especially combined with the "local", it looks like I'm trying to get the length of a variable, rather than declaring a private field.

This syntax is confusing, hard to read, and provides little to no benefit other than a small performance boost, which I believe can also be achieved using different workarounds.

That aside, the problem of private constructors becoming difficult to implement have arisen immediately after reserving .new(), just like I guessed. It is mind boggling that the team still wishes to keep it when in reality it will give them a hard time implementing features.

Apologies if the framing of the RFC made it seem like the glyph approach was chosen for performance reasons. Our main concern was the semantic ambiguity that falls out of allowing child classes to shadow private fields in their parents with their own public fields.

That being said, we understand the concern around the particular glyphs and keywords we chose, and are happy to discuss alternatives in this space!

@Bottersnike

Copy link
Copy Markdown

In terms of that ambiguity, is that something that ever actually pops up? At least from personal experience I don't think I've ever found it to be a problem in other languages? There's also the consideration that editors can syntax highlight privates differently to publics which brings back the "easy way to tell if it's public or private when reading code" part.

@TenebrisNoctua

Copy link
Copy Markdown

Let's talk about the syntax.
The "local" and "#" being used to declare public or private fields is completely awful. The RFC proposes them as an alternative to a potential performance issue with actual access specifiers.
First of all, what does even "local" mean in the context of classes? Are they local variables? Or fields? Are they the same as our normal local? Will the user be able to differentiate between them?
And the "#" symbol used for private fields is horrible, especially combined with the "local", it looks like I'm trying to get the length of a variable, rather than declaring a private field.
This syntax is confusing, hard to read, and provides little to no benefit other than a small performance boost, which I believe can also be achieved using different workarounds.
That aside, the problem of private constructors becoming difficult to implement have arisen immediately after reserving .new(), just like I guessed. It is mind boggling that the team still wishes to keep it when in reality it will give them a hard time implementing features.

Apologies if the framing of the RFC made it seem like the glyph approach was chosen for performance reasons. Our main concern was the semantic ambiguity that falls out of allowing child classes to shadow private fields in their parents with their own public fields.

That being said, we understand the concern around the particular glyphs and keywords we chose, and are happy to discuss alternatives in this space!

Updated the reply to be more clear, sorry about that!

@skberkeley
skberkeley force-pushed the skberkeley/private-fields branch from 20d61f2 to 466dae2 Compare August 19, 2026 16:15
@skberkeley

Copy link
Copy Markdown
Member Author

Closed in favor of #247

@skberkeley skberkeley closed this Aug 19, 2026
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

Successfully merging this pull request may close these issues.

8 participants