Skip to content

Class Constructors - #210

Open
andyfriesen wants to merge 4 commits into
masterfrom
class-constructors
Open

Class Constructors#210
andyfriesen wants to merge 4 commits into
masterfrom
class-constructors

Conversation

@andyfriesen

@andyfriesen andyfriesen commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Introduce class constructors via special .__init and .new() methods.

Rendered

See #229 for the main discussion thread on classes as a whole.

@TenebrisNoctua

Copy link
Copy Markdown

Honestly I'm kind of torn by this RFC, it defines a lot of new rules and restrictions just to work around the problem that comes with inheritance, can't a static keyword be implemented in its place instead to allow a class to only define a static field which will not be effected by inheritance? The developers will have to keep in mind of the rules when defining constructors, and that may confuse some people within the ecosystem.

@andyfriesen

Copy link
Copy Markdown
Collaborator Author

Honestly I'm kind of torn by this RFC, it defines a lot of new rules and restrictions just to work around the problem that comes with inheritance, can't a static keyword be implemented in its place instead to allow a class to only define a static field which will not be effected by inheritance?

Could you explain how a static keyword could help? I don't understand.

The restrictions exist to ensure that you can't interact with an object that isn't fully constructed yet. If you write a field public t: thread, it isn't actually a thread until you initialize it.

@deviaze

deviaze commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This adds too much complexity to the language, comes with a bunch of restrictions and rules you need to know to use properly, and isn't obvious.

An __init function that gets called as new is confusing, I just want to write a new function like in Rust and be done with it.

Inheritance is a controversial feature that's discouraged in modern language design, and I don't think that supporting it like this is the right choice for Luau. Has a different approach like composition been investigated?

@TenebrisNoctua

Copy link
Copy Markdown

Honestly I'm kind of torn by this RFC, it defines a lot of new rules and restrictions just to work around the problem that comes with inheritance, can't a static keyword be implemented in its place instead to allow a class to only define a static field which will not be effected by inheritance?

Could you explain how a static keyword could help? I don't understand.

The restrictions exist to ensure that you can't interact with an object that isn't fully constructed yet. If you write a field public t: thread, it isn't actually a thread until you initialize it.

My comment on static was more or less about the first section of the RFC, which brings up the problem with inheritance for the .new function.

A static access modifier would bring in the capability of defining static methods/fields for a class, which cannot be inherited by a subclass. This would solve the problem with each class having their own non-inherited static constructor function.

Assuming functions with static keyword (or no self argument) are static and cannot be inherited, doesn't this sound like a good way of allowing the user to create their own constructors without reserving any names:

class Base
    public foo: string
    static function new(newObj, str: string)
        if not newObj then
            return Base { foo = str }
        end
        newObj.foo = str
    end
end

class SubFromBase extends Base
    static function new()
        local newObj = SubFromBase { } -- Assuming this is allowed, you move the magic from __init to here
        Base.new(newObj)
        -- Add and initialize other properties as you would
    end
end

For every child of SubFromBase must define their own static constructor function that may call SubFromBase, which calls Base.

Comment thread docs/class-constructors.md Outdated

## Alternatives

We could follow in Python's footsteps and do away with the default `T{}` constructor, but this means that developers have to write a lot of dull code in the typical "plain old data" case:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would actually prefer this even more, as the way "default table constructors" for classes currently work really irritates me.

First, as I've said before, the current design conflates the idea that the fields of a class are also its constructor. This poses awkward questions regarding how private and static fields are treated and is just a big mismatch with the current syntax. Languages that lay out their class fields in a spreadsheet manner make the programmer write out the constructor by hand, but Luau doesn't and generates one automatically, which is weird.

Second, the table constructor protocol requires at a bare minimum to allocate a throwaway table that is then passed to a C function that then sanitizes and allocates the real object from it. This implies that baseline performance for construction will always be worse than just allocating a regular table. The justification that future compiler heroics will cut back on the performance hit feels more like a band-aid solution, and I'd expect a stronger foundation for the runtime side than that.

Third, this RFC is proposing an alternative constructor protocol that supersedes most if not all uses for the default table constructor. If that's the case, why even support table construction to begin with? If someone feels like the constructor boilerplate is "pointless ceremony" for a POD datatype, then maybe the syntax for classes needs to be revisited to incorporate the idea of default constructors better.

@TenebrisNoctua TenebrisNoctua Jun 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Honestly, I think a C++ like syntax would be more ideal, where the class object can still be called like a function, however you do not allocate a table for initializing the fields. Instead, you pass arguments to this function, which if defined, will pass these arguments to a constructor. If there is no constructor, then the fields will not be initialized, and therefore nil.

class Foo
    protected x: number
    public function Foo(self)
        self.x = 51
    end
end

class Bar extends Foo
    public function Bar(self)
        Foo(self)
    end
end

local newFoo = Foo()
local newBar = Bar()

Foo() now applies magic (the same in the proposed __init) and constructs a new, but uninitialized instance of the class. This instance is then passed to the constructor, in which the function can use to initialize its fields.

However, this constructor can also be called with another object which is made within a subclass, in which the constructor's self parameter points to this object, rather than a new instance of Foo.

One problem with this could be that there is an ambiguity between the argument types. To solve that problem, Luau could check whether or not the first argument to the class function is an object which is made from a subclass.

class Foo
    protected x: number
    public function Foo(self, x: number)
        self.x = x
    end
end

class Bar extends Foo
    private y: string
    public function Bar(self, x: number, y: string)
        Foo(self, x)
        self.y = y
    end
end

local newFoo = Foo(51)
local newBar = Bar(51, "Hello")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, that's sort of what I had in mind as well, minus the inheritance bit. Anything but a default table constructor so that the class can be truly encapsulated should private and static fields be added.

And for POD cases, "record"-style syntax sugar could be added which generates a constructor automatically, i.e. class Point(x: number, y: number) end.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It feels similar to the proposal in this RFC, but with better semantics and you can still use .new() however you please. I wonder if this is worthy of an amendment RFC, or should be included in this RFC instead.

@Bottersnike

Copy link
Copy Markdown

I don't understand the rationale with __init and new. I can understand the name __init to follow metamethod-style, but in that case I would expect a "special" way to initialise, such as MyClass(...) calling __init. Having new magically mapped feels.. off. This RFC expressly disallows new being defined as a function, but then has users define new anyway, just writing "new" as "__init".

It feels like in the battle between two options (__init->T() or new->.new()) the worst middle-ground compromise has been picked here.

We draw inspiration from Python

B.__init(self, x)

Something worth noting is Python relatively quickly (in the scheme of python development times) realised this wasn't ideal, and ended up with super().__init__(...). Given we've got the benefit of hindsight, it could be worth designing something akin to this in from the start?

The first argument of a constructor must be self.

Are we walking a slippery road with starting to make variable names formally required? If the first argument to a function must be named self would it not make more sense to entirely omit it, and have it injected in (akin to a namecall)? Alternatively, allow it to be named anything of the user's choosing. The middle ground of an enforced variable name in a function argument seems particularly weird.

If the base class defines a constructor, the class constructor must invoke it before reading or writing to self or any of its properties

This seems hard to enforce? Is there a reason it's required? The base class would be assuming an empty table, so isn't going to be reading from fields anyway? If default field values happen, it feels like we have added value by allowing a subclass to overwrite the already-initialised defaults before calling a base constructor? I do agree most uses would invoke the superclass constructor as the first step of their constructor, but is it a required restriction?

As a special exception, fields whose types are supertypes of nil are exempt from this requirement and are always considered to have been implicitly initialized with nil. Explicit initialization of such fields is of course permitted.

This almost points towards "we have default values, but the only default you can assign is nil". This of course comes part-and-parcel with the semantics of Lua(u) tables, but strikes me as a call-out that indicates a wider lack of defaults? It might almost, at least to me, make sense to not have this exception, and require optional fields to either be explicitly initialised as self.foo = nil or defined as foo: number? = nil in the declaration.

I only raise this because this is the sort of exception that shouldn't really be walked back once released (though as I understand it this enforcement is only via types, so absolutely could have "breaking" changes).

--

In terms of general implementation inheritance, how does __init interact with inherited methods/metamethods? Is __init inherited like an ordinary method, or is it special-cased as constructor-only? Metamethod inheritance was not covered in #209, so it is unclear whether this relies on metamethods not being inherited, or whether __init has bespoke rules.

Also, if a subclass defines no new fields and does not need additional initialization, must it still define a constructor that simply delegates to the parent constructor?

@TenebrisNoctua

Copy link
Copy Markdown

I don't understand the rationale with __init and new. I can understand the name __init to follow metamethod-style, but in that case I would expect a "special" way to initialise, such as MyClass(...) calling __init. Having new magically mapped feels.. off. This RFC expressly disallows new being defined as a function, but then has users define new anyway, just writing "new" as "__init".

It feels like in the battle between two options (__init->T() or new->.new()) the worst middle-ground compromise has been picked here.

We draw inspiration from Python

B.__init(self, x)

Something worth noting is Python relatively quickly (in the scheme of python development times) realised this wasn't ideal, and ended up with super().__init__(...). Given we've got the benefit of hindsight, it could be worth designing something akin to this in from the start?

The first argument of a constructor must be self.

Are we walking a slippery road with starting to make variable names formally required? If the first argument to a function must be named self would it not make more sense to entirely omit it, and have it injected in (akin to a namecall)? Alternatively, allow it to be named anything of the user's choosing. The middle ground of an enforced variable name in a function argument seems particularly weird.

I don't think the first parameter must always be named self, but rather something that points to the object. Should've been made more clear to be honest.

I pretty much agree on the other points, incredibly weird and complex workaround just to achieve constructors and support inheritence. Semantics could've been so much better if table initialization did not exist and instead of new, only the class name as the constructor was reserved. Instead of Python, we could follow C++.

@InfraredGodYT

Copy link
Copy Markdown

I also think that mirroring Python isn't the way to go. __init looks like a metamethod rather than a constructor. Not to mention the weird split in syntax between classes that have constructors and classes that don't.

-- Has constructor:
T.new(...)

-- Doesn't have constructor:
T { x = 1, y = 2 }

I'm also not sure why constructor and super weren't mentioned in the Alternatives section.

@TenebrisNoctua

TenebrisNoctua commented Jun 5, 2026

Copy link
Copy Markdown

I've noticed that the class instance initialization method in POD does not work well with constructors, especially with inheritance. Because of this, I'd like to propose alternative syntax and semantics for constructors and class instance initialization.

While this requires possibly amending the already existing class syntax, it could be better for the feature overall.

Instead of Foo { ... }, the user initializes the properties within constructors. This means that Foo() should now be used instead to construct an uninitialized instance of a class.

Foo() also now gains two behaviors:

If there is no constructor (__init) present:

class Foo
    public x: number
end

local newFoo = Foo() -- If no constructor exists, this initializes a new uninitialized instance of the class Foo.
newFoo.x = 1

If there is a constructor:

class Foo
	public x: number
	
	function __init(self, x: number)
		self.x = number
	end
end

local newFoo = Foo(1) -- This calls the constructor with the parameters

If there exists a constructor defined, Foo() will now call the constructor with an already created, albeit still uninitialized instance of class Foo.
While this does introduce magic, it removes the table allocation problem completely from classes, and gives user total control over initialization.

For inheritance

In C++, parent class constructor is automatically called before the derived class constructor, if they don't require any additional arguments.
Instead of following Python, perhaps C++ can be followed instead, and in many cases this could solve our parent class constructor problem.

class Base
	function __init(self)
		print("Base class called!")
	end
end

class Derived extends Base
	function __init(self)
		print("Derived class called!")
	end
end

local newDerived = Derived()
-- The output would be:
-- "Base class called!"
-- "Derived class called!"

Of course, when it comes to passing parameters to parent class constructors, things get a little bit tricky, as in C++, you can define an initializer list, however to do that in Luau, additional syntax would have to be implemented.
The RFC states that additional syntax is not desired when we already are using a lot, so this may not be desirable.

We could still move on with the rule that if the base class defines a constructor, the class constructor must invoke it before reading or writing to the object or any of its properties:

class Base
	public x: number
	function __init(self, x: number)
		self.x = x
	end
end

class Derived extends Base
	function __init(self, x: number)
		Base.__init(self, x)
	end
end

local newDerived = Derived(1)

We could also re-consider super, which in Python allows the user to reference the methods of the parent class, without explicitly needing to directly refer to the parent class.

class Base
	public x: number
	function __init(self, x: number)
		self.x = x
	end
end

class Derived extends Base
	function __init(self, x: number)
		self:super().__init(self, x)
	end
end

local newDerived = Derived(1)

super could also just directly refer to the parent method/constructor like:

class Base
	public x: number
	function __init(self, x: number)
		self.x = x
	end
end

class Derived extends Base
	function __init(self, x: number)
		self:super(x)
	end
end

local newDerived = Derived(1)

Overall, I believe these proposed syntax changes would make this RFC more solid, and would make writing classes with inheritance and constructors a lot easier. It may also remove the table allocation problem, which would be a nice bonus.

@Bottersnike

Copy link
Copy Markdown
local newFoo = Foo() -- If no constructor exists, this initializes a new uninitialized instance of the class Foo.

This feels like really unintended behaviour. newFoo.x is typed as number, but is now actually nil!? I think it might be better to instead have a "default constructor" that's just the POD thing we have right now, because that way we never get into a situation where an object doesn't match the types.

Regarding inheritance, the first example given is imo untenable because of the exact issue raised there: constructor arguments. I think having to call Base.__init() is definitely the better option.

I also agree with the addition of super, but how it would be done I'm not sure. self:super() isn't really viable unless we reserve the method name super, but that feels like a nasty workaround. class.super(self).__init() could work, though is potentially a bit wordy?

I think super, in whatever shape it takes, should refer to the parent class, not directly to its constructor, because then it can also be used to call parent implementations of methods that have been overridden in a child.

@TenebrisNoctua

TenebrisNoctua commented Jun 5, 2026

Copy link
Copy Markdown
local newFoo = Foo() -- If no constructor exists, this initializes a new uninitialized instance of the class Foo.

This feels like really unintended behaviour. newFoo.x is typed as number, but is now actually nil!? I think it might be better to instead have a "default constructor" that's just the POD thing we have right now, because that way we never get into a situation where an object doesn't match the types.

Honestly this could be avoided if we had default values for fields. As mentioned in the RFC, in certain cases types may not match the values, especially when the self argument points to an uninitialized object, which means x will be nil regardless. Perhaps another behavior could be found where we still do not have to allocate a table for initialization.

I also agree with the addition of super, but how it would be done I'm not sure. self:super() isn't really viable unless we reserve the method name super, but that feels like a nasty workaround. class.super(self).__init() could work, though is potentially a bit wordy?

super could also be a contextual thing, because class.super(self)._init() is very wordy. And adding it under class library would be weird.

@Bottersnike

Copy link
Copy Markdown

One challenge with super that just came to mind is the types. Given I don't think the plan is to have some magic base object type (a-la python, roblox itself, etc), the superclass of a class that doesn't inherit anything would be nil. That means that instead of super().__init(), and in the absence of safe navigation, we'd need it typed having an optional return, leading to

local sup = super()
if sup then sup.__init() end

I unfortunately don't have a good solution for that in mind. The base object type idea is quite reliable but probably undesired. One option would be to make it error, instead of returning nil. I'm unsure to what degree this would be desired.

--

In terms of class.super, one power it would provide as a member of class is being able to inspect the inheritance chain of something; this could be useful?

@TenebrisNoctua

Copy link
Copy Markdown

In my own class system, super() errors if the class does not have a superclass. Not sure if people would desire this like you have mentioned, but erroring could be better than nil.

That aside, now that I'm thinking about it, if we had super under the class library, semantics could be better.

const super = class.super
class Base
    function __init(self)
    end
end

class Derived extends Base
    function  __init(self)
        super(Derived).__init(self)
    end
end

Still unsure though.

@Cooldude2606

Copy link
Copy Markdown

Classes that have constructors cannot be constructed via T() syntax. Instead, the static method .new must be invoked. Classes that do not define constructors are still initialized via T {...} syntax.

This is a very confussion restriction to impose with no justification presented. I would instead propose that using T {...} syntax ("table call") calls a default __init method which can be optionally replaced with a new implimentation. With metatables it might have looked something like the following:

-- T { ... }
function class_mt.__call(class, ...)
    local self = {}
    class.__init(self, ...)
    return self
end

-- T.__init(self, args)
function class.__init(self, args)
    self.x = args.x
    self.y = args.y
end

This maintains the current behaviour of using a table call to create a new instance of a class while permitting new implimentions to extend behaviour. There is no requirement for this implimentation to use table calls as one may instead use __init(self, x, y) and call via T(x, y); although if defaults with table calls are optimised (which I see no reason why they would not be, see last paragraph) then this parttern would become a performance footgun because of the additional function call.


In terms of typing for super classes, I think class.super would be nice but can be implimented in a different RFC. It will be sufficent at the moment to call the parent class directly within class methods, with "super" being a useful QoL addition to be agreed on later. Although I will chip in and say I agree that it should error if the class has no parent, and any external introspection can wrap it in pcall. local hasSuper, super = pcall(class.super, self)


Assuming the above, I want to suggest that any replacement init method must refine the original self argument into a form which fulfils the class type, or else results in a lint error. There is no requirement that the parent init is called; however, if it is not, then the child init is now responsible for completing the initialisation of the member fields. Additionly, there is no restriction on reading or writing of any member fields, but they will be typed as nil-able if they have not been refined by the parent init. Apologies for the use of typescript, luau has no "assert type predicate", but I would imagine it as the following:

function __init<
    Self extends Partial<{ x: number; y: number }>
> (
    self: Self,
    args: { x: number; y: number },
): asserts self is Self & { x: number; y: number };

I saw there was another RFC which suggested the addition of type predicate functions, and so that may need to be revisted to enable this kind of type refinement on arguments. But anyway, it would work something like so within lua:

class Point2D
    public x: number
    public y: number
    -- no __init implimented, the default is used, which matches the metatable example above
end

class Point3D extends Point2D
    public z: number
    
    function __init(self, args: { x: number, y: number, z: number })
        -- self: { x: number | nil, y: number | nil, z: number | nil }
        Point2D.__init(self, args) -- It is still callable as a default is generated or inlined
        -- self: { x: number, y: number, z: number | nil }
        self.z = args.z
        -- self: { x: number, y: number, z: number }
        -- because self is the correct type, no lint error is rasied about incomplete field init
    end
end

As an extension, it is possible that Point3D.__init could also be seen as “default”, which is to say: a call to a default parent init where you pass your args, then extract your new members from the table. This would allow for simple heirarchries of classes without needing to specify any custom implimentations. Also, because it is suggested that child classes have their fields ordered the same as the parent, it would be trivial for a full chain of defaults to be inlined without any table creation or function calls at all. This maintains the current optimisation that could be applied to current class construction, so long as no class in the heirarchery have a custom implimention of init, in which case it would need to fallback to compiling as a function call to init. Some made up byte code as food for thought on how defaults could compile:

Unoptimised bytecode where __init is never inlined
; --- call site: constructing with Point3D { x = 1, y = 2, z = 3 }  ---

[1] LOADK           0 1            ; x = 1
[2] LOADK           1 2            ; y = 2
[3] LOADK           2 3            ; z = 3

[4] NEWCLASS        3 K0           ; t = new <Point3D>

[5] NEWTABLE        4 0 3
[6] SETFIELD        4 "x" 0
[7] SETFIELD        4 "y" 1
[8] SETFIELD        4 "z" 2

[9] GETFIELD        5 K0 "__init"
[10] MOVE           6 3            ; self
[11] MOVE           7 4            ; args
[12] CALL           5 3 1          ; __init(self, args)


; --- Point3D.__init(self, args) ---

[1] GETFIELD        2 K1 "__init"  ; Point2D.__init
[2] MOVE            3 0            ; self
[3] MOVE            4 1            ; args
[4] CALL            2 3 1

[5] GETFIELD        2 1 "z"
[6] SETCLASSFIELD   0 2 2          ; self.z = args.z

[7] RETURN          0 1


; --- Point2D.__init(self, args) ---

[1] GETFIELD        2 1 "x"
[2] SETCLASSFIELD   0 0 2          ; self.x = args.x

[3] GETFIELD        2 1 "y"
[4] SETCLASSFIELD   0 1 2          ; self.y = args.y

[5] RETURN          0 1
Optimised bytecode where calling the parent requires a custom implimention
; --- call site: constructing with Point3D { x = 1, y = 2, z = 3 } ---

[1] LOADK           0 1            ; x = 1
[2] LOADK           1 2            ; y = 2
[3] LOADK           2 3            ; z = 3

[4] NEWCLASS        3 K0           ; t = new <Point3D>

[5] NEWTABLE        4 0 3
[6] SETFIELD        4 "x" 0
[7] SETFIELD        4 "y" 1
[8] SETFIELD        4 "z" 2

[9] GETFIELD        5 K0 "__init"
[10] MOVE           6 3            ; self
[11] MOVE           7 4            ; args
[12] CALL           5 3 1          ; __init(self, args)


; --- Point3D.__init(self, args) with Point2D default inlined ---

; inlined Point2D.__init (default)
[1] GETFIELD        2 1 "x"
[2] SETCLASSFIELD   0 0 2          ; self.x = args.x

[3] GETFIELD        2 1 "y"
[4] SETCLASSFIELD   0 1 2          ; self.y = args.y

; Point3D-specific field
[5] GETFIELD        2 1 "z"
[6] SETCLASSFIELD   0 2 2          ; self.z = args.z

[7] RETURN          0 1
Fully optimised bytecode where calling the parent is "default"
; --- fully optimised: no __init calls or table, fully inlined default chain ---

[1] LOADK           0 1            ; x = 1
[2] LOADK           1 2            ; y = 2
[3] LOADK           2 3            ; z = 3

[4] NEWCLASS        3 K0           ; t = new <Point3D>

[5] SETCLASSFIELD   3 0 0          ; self.x = x
[6] SETCLASSFIELD   3 1 1          ; self.y = y
[7] SETCLASSFIELD   3 2 2          ; self.z = z

Comment thread docs/class-constructors.md Outdated
class A extends B
public x: number

function __init(self, x, y)

@alexmccord alexmccord Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is so much like Python's __postinit. Then you'd only write the last line there, and the constructor call probably has to invoke this metamethod anyway. Then there's no need for T.new being sugar for __init (not to mention what happens if the function new exists along with __init).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As in a dataclasses __post_init__? It feels much more akin to __init__ given a DC's __post_init__ only runs after initialisation, where you'd do (in Luau class terms) A { x = 1, ... }, rather than a custom A(some, arguments, here) constructor, then the post-init gets called on an already-populated object. This proposal seems to be focusing on self being entirely uninitialised at the start of __init, which would just be a normal __init__ on a python class.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, __post_init__ from Python's @dataclass, yeah. But re-reading this RFC, this specific class is better done with __post_init__ but the general problem is still not solvable with that one. Constructors vs new really does need to exist (kinda).

class A extends B
public x: number

function __init(self, x, y)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, __post_init__ from Python's @dataclass, yeah. But re-reading this RFC, this specific class is better done with __post_init__ but the general problem is still not solvable with that one. Constructors vs new really does need to exist (kinda).

Comment thread docs/class-constructors.md Outdated
public name: string

function new(): DerivedPoint
-- We're stuck! We cannot implement this function in terms of BasePoint.new()

@alexmccord alexmccord Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think so? DerivedPoint { x = 1, y = 2, name = "hello" }. The difference is that if the superclass has some field x and the subclass also has some field x, then that subclass must shadow that field x. This is only a problem if you need the side effects from BasePoint.new() or it has private fields. And in both cases, the problem stems entirely from inheritance in the first place.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This argument only stands with "closed universe" assumption when whole hierarchy defined in the same code base. You can image some LibBaseClass(e.g. ReactElement) and UserExtensionClass extends LibBaseClass. In this case every update of library code with addition of fields to LibBaseClass will break UserExtensionClass definition.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, but I'd also argue that the open world assumption is problematic, hence why I suggested this.

Consider a superclass with a bunch of fields and methods with default implementation. If someone extends from it, and adds a field that shadows the field in the superclass, then all methods defined in the superclass that depends on that field are now logically broken.

If there's no open world assumption, which is what inheritance implies, then this entire category of problems disappears.

Comment thread docs/class-constructors.md Outdated

Some developers will feel inconvenienced by the restrictions on uninitialized class fields. The current rules do not, for instance, permit a developer to write a helper function that partially (or completely) initializes a new class instance.

## Alternatives

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Another alternative (that I already liked, but am starting to really like it) is "Do not support inheritance" and now this entire category of problems are gone.

@karl-police

Copy link
Copy Markdown
Contributor

What about __ctor instead of __init 🤔

The most significant change is that we do away with the separate
notion of "POD classes."  All classes now provide `.new()` and
`.__init()` methods for allocation and construction.

The old `T { ... }` syntax is now written `T.new { ... }` instead.
@andyfriesen andyfriesen mentioned this pull request Jul 16, 2026
@andyfriesen

Copy link
Copy Markdown
Collaborator Author

I've made some simplifications to the design here. Now, all classes always provide .new() and .__init() methods. If a class does not explicitly define __init itself, one is provided by the runtime. This default constructor takes the mapping of properties to their values.

This makes things much more consistent in the presence of inheritance.

@deviaze

deviaze commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

The RFC now uses open class syntax in an example, which isn't mentioned everywhere and now refers to the language as Lua instead of Luau. Is this intentional?

@deviaze

deviaze commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

If users can override __init but not .new would the default new just break?? Can we define our own new or is it reserved by the language? Why would we want to override __init when all it does is create an initialized but not populated instance of the class? Imho most people would want to change new to like.. not accept a table, or accept whatever params they want. I don't see how this fixes the problem.

@andyfriesen

Copy link
Copy Markdown
Collaborator Author

The RFC now uses open class syntax in an example, which isn't mentioned everywhere and now refers to the language as Lua instead of Luau. Is this intentional?

It's described here: #230

If users can override __init but not .new would the default new just break?? Can we define our own new or is it reserved by the language? Why would we want to override __init when all it does is create an initialized but not populated instance of the class? Imho most people would want to change new to like.. not accept a table, or accept whatever params they want. I don't see how this fixes the problem.

new() is reserved by the language. It always passes its arguments on to __init:

class Point
    public x: number
    public y: number

    function __init(self, x, y)
        self.x = x
        self.y = y
    end
end

local p = Point.new(2, 3)

@Bottersnike

Bottersnike commented Jul 16, 2026

Copy link
Copy Markdown

I still find it quite weird that between Foo() -> function __init and Foo.new() -> function new we've got Foo() -> function new.

I can see the justification for .new() being the constructor to make code look more like existing Lua(u) code, though imo that's a relatively weak justification when adding an entirely novel concept in the language anyway. If the team is absolutely set on .new() regardless, I think it would make a lot more sense for the constructor to just be defined as function new.

The latest edit changed the problematic example that used function new to use function create, but by doing so entirely sidestepped the problem of what

class Foo
  function new() end
end

does.

In a similar vein, what does Foo.__init do? Is __init just renamed to new internally?

Clarify that the method name `new()` is reserved.
@andyfriesen

Copy link
Copy Markdown
Collaborator Author

In a similar vein, what does Foo.__init do? Is __init just renamed to new internally?

The behaviour of new() is described here.

The extra magic that new() contributes is to fabricate a fresh, uninitialized value to run the constructor on. The distinction is necessary because there are two important operations that we need to be able to talk about separately:

T.new() -- allocate and initialize an object
T.__init(self) -- run a constructor on an existing object

@Bottersnike

Copy link
Copy Markdown

Is running a constructor on an existing object a meaningful operation? What would the use-case for that be?

@andyfriesen

Copy link
Copy Markdown
Collaborator Author

Is running a constructor on an existing object a meaningful operation? What would the use-case for that be?

The most important use case is running a base class constructor.

class FancyPoint extends Point
    public name: string

    function __init(self, x, y, name)
        Point.__init(self, x, y)
        self.name = name
    end
end

@Bottersnike

Bottersnike commented Jul 16, 2026

Copy link
Copy Markdown

That does make sense. I fear the risk of being being able to run the constructor at any point on any object feels very high. I know the inheritance RFC states

We considered a super keyword, but found the justification for it to be very weak. The developer can just write the actual name of the base class

Which suggests the only rationale would be to avoid the Foo in Foo.__init(), but this mix of new and __init combined with being able to re-call a constructor at any point feels like a much stronger rationale for adding a super().

The new/__init split is very similar to what Python has with the static member __new__ and the bound member __init__, but in Python land that split is very cleanly hidden away, and the cases where __new__ gets overwritten or even thought about are exceedingly few. I think where Python gets that split right is that __new__ is actually what gets called when you do Foo(), but for practically all purposes you never think about that, and it's "actually" just calling __init__; most Python developers probably aren't even aware of __new__ or how the memory allocations it performs work. It also elegantly avoids the problem of function new() ... end.

@TenebrisNoctua

TenebrisNoctua commented Jul 16, 2026

Copy link
Copy Markdown

It would've been nice if it was possible to rename __init() to new for consistency with .new(), but alas this is a lot better than what was previously proposed.

Still not sure about inheriting constructors though. There is a case in strict typechecking that if the base class defines a constructor but the child doesn't, it creates a warning, which is fine but it would be better if constructors didn't inherit in my opinion.

Which suggests the only rationale would be to avoid the Foo in Foo.__init(), but this mix of new and __init combined with being able to re-call a constructor at any point feels like a much stronger rationale for adding a super().

I do heavily agree with this. While in the previous version of the RFC, since we didn't have .new(), __init was fine as a name. But now, there is this inconsistency at the name of the method you're calling from the class object versus the actual method that is being called, which has a different name.

@andyfriesen

Copy link
Copy Markdown
Collaborator Author

That does make sense. I fear the risk of being being able to run the constructor at any point on any object feels very high. I know the inheritance RFC states

We considered a super keyword, but found the justification for it to be very weak. The developer can just write the actual name of the base class

Which suggests the only rationale would be to avoid the Foo in Foo.__init(), but this mix of new and __init combined with being able to re-call a constructor at any point feels like a much stronger rationale for adding a super().

Python has you write super().__init__(args...). They use some kind of magic to spare you from needing to specify self.

I think the justification for super() is much stronger in Python because it supports multiple inheritance. You can use super() to run every base class constructor.

The new/__init split is very similar to what Python has with the static member __new__ and the bound member __init__, but in Python land that split is very cleanly hidden away, and the cases where __new__ gets overwritten or even thought about are exceedingly few. I think where Python gets that split right is that __new__ is actually what gets called when you do Foo(), but for practically all purposes you never think about that, and it's "actually" just calling __init__; most Python developers probably aren't even aware of __new__ or how the memory allocations it performs work. It also elegantly avoids the problem of function new() ... end.

I'm glad you noticed! This proposal is basically the same except we simplify by declining to offer an analogue to __new__. There is only the public construction interface (T() in Python, T.new() in Luau) and the constructor definition (T.__init__() in Python, T.__init() in Luau)

@Bottersnike

Bottersnike commented Jul 16, 2026

Copy link
Copy Markdown

The magic python has is just bound vs unbound methods. You can do Foo.__init__() if you want, super() is only needed in the case of multiple inheritance (it'll resolve the constructor MRO for you).

I think what Python gets right here in terms of making it feel like it magically works, rather than feeling weird, is exactly the fact that constructors are a magic Foo() rather than looking like a static method call (Foo.new()) for a static method that you can never redefine. I think specifically the desire to stick with .new() is what's making this design feel very at odds with itself.

To give a bit of an example, even a class.newinstance(Foo) would no longer have the same "weird" feel as Foo.new(), albeit obviously not a nice design. new vs __init feels like the sort of thing that when being described to someone who's not "turned on" regarding language internals would need hand-waved as "don't think about it, just always use function __init and never function new but then call Foo.new()" which feels awful to have to tell people.

@deviaze

deviaze commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

It is now even more confusing that you need to override __init not new, but then the default new calls __init and somehow magically knows when to use the table call version vs. a custom one?

I agree with Bottersnike, the fact that new is such a commonly-wanted function that looks like it'd be easy to redefine, that the way to change it is by overriding __init instead of new, and there isn't special syntax like C() or new C for something that's a metamethod... It works in Python because __init is mapped to Class(). I hate it in Ruby that def initialize actually maps to Class.new (it's confusing), and this makes a similar mistake.

I would expect there to be a way to get a fresh, unpopulated instance of a class, that could be a private magic metamethod that cannot be overridden, and then use it in your custom constructor that defines how the properties are set.

We could name it __uninit. And then, as a user defining a class that should not get inherited, you'd simply do:

class LineComment
	public text: string
	public line: number
	public column: number
	-- I don't like the default .new, let me make my own
	function new(text: string, position: vector)
		const self = LineComment.__uninit() -- private to class block
		self.text = text
		self.line = position.y
		self.column = position.x
		return self
	end
end

If it's one that can be inherited, you need an __init metamethod to specify default initialization.

open class Snake
	public length: string
	public dangerous: boolean
	
	function __init(length, dangerous)
		const self = Snake.__uninit()
		self.length = length
		self.dangerous = dangerous
	end
end

class Python extends Snake
	public name: string
	
	function new(name, length)
		const self = Snake.__init() -- or a special syntax
		self.name = name
		self.length = length
		return self
	end
end

This would make it less confusing what's actually happening.

@TenebrisNoctua

TenebrisNoctua commented Jul 16, 2026

Copy link
Copy Markdown

To give a bit of an example, even a class.newinstance(Foo) would no longer have the same "weird" feel as Foo.new(), albeit obviously not a nice design. new vs __init feels like the sort of thing that when being described to someone who's not "turned on" regarding language internals would need hand-waved as "don't think about it, just always use function __init and never function new but then call Foo.new()" which feels awful to have to tell people.

We need to consider this feature from the eyes of the users who will use this. The difference between __init and new may not confuse us, who are very versed within the development and even internals of the language, but for anyone else, they may be quite confused.

@deviaze

deviaze commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

We need to consider this feature from the eyes of the users who will use this. The difference between __init and new may not confuse us, who are very versed within the development and even internals of the language, but for anyone else, they may be quite confused.

I disagree--the difference confuses me already, and I've been versed in Luau for years and know quite a bit about its internals. I can't imagine how confusing it'd be to new users and regular users.

I think any default constructor should be exposed as magic syntax or a metamethod, and that users should be able to name constructors whatever they want. The fact is, thanks to existing conventions they'll name them new anyway. If you don't want to have a method named new at all, it should be your choice.

@Bottersnike

Bottersnike commented Jul 16, 2026

Copy link
Copy Markdown

To give a bit of an example, even a class.newinstance(Foo) would no longer have the same "weird" feel as Foo.new(), albeit obviously not a nice design. new vs __init feels like the sort of thing that when being described to someone who's not "turned on" regarding language internals would need hand-waved as "don't think about it, just always use function __init and never function new but then call Foo.new()" which feels awful to have to tell people.

We need to consider this feature from the eyes of the users who will use this. The difference between __init and new may not confuse us, who are very versed within the development and even internals of the language, but for anyone else, they may be quite confused.

That was largely my point there. I could fully seem something like this being explained on the dev forum as "just don't think about it, and use this specific pattern" rather than attempts to try and explain why they're different and why .new is calling into __init and why it would be nonsensical to define your own function new.

By having .new() this is inevitably a question that will be asked, because it's waving itself at you just begging for curiosity, rather than being hidden away.

@TenebrisNoctua

Copy link
Copy Markdown

I disagree--the difference confuses me already, and I've been versed in Luau for years and know quite a bit about its internals. I can't imagine how confusing it'd be to new users and regular users.

I think any default constructor should be exposed as magic syntax or a metamethod, and that users should be able to name constructors whatever they want. The fact is, thanks to existing conventions they'll name them new anyway. If you don't want to have a method named new at all, it should be your choice.

I can agree with this for sure. Personally, a static method called new that exists by default on all classes that can be overridden by the user seems like the best choice. I don't agree with constructor inheritance nor this weird naming convention, it creates additional complexity for no important reason.

@TenebrisNoctua

Copy link
Copy Markdown

I propose an amendment to this RFC, to increase the capabilities of constructors and remove some pain-points that people have presented above.

I want to propose the addition of new rules and removal of certain others, to ensure classes are ready for any possible future additions without going through the mess we went through before:

Do not reserve .new() on the class object.

The Roblox ecosystem for many years have chosen "new" as the name of their constructor functions in their classes. This is a well known fact, and the team wishes to uphold this by reserving "new" as a reserved method which must be called when creating an instance from a class. This may seem like a good idea at first, but there are a couple of important problems which arises with this.

First of all, the language should not reserve a method simply because the ecosystem uses it, if the ecosystem wants to use it, they will define it themselves. We are not introducing a significant semantics update that will make a significant change in anyone's codebase. Classes themselves bring in a whole new syntax with its own rules and semantics, users will have to spend time adapting, this is inevitable.

Second, it is a limitation that brings in confusion. When you present it to an user of this language, and they wish to understand the idea behind .new(), how are we supposed to explain it to them? Is the reason of "just for semantics" enough? The user will have to accept that .new() is a reserved magic function which cannot be modified, and calls __init eventually. It will certainly create confusion.

Third, it limits the potential capabilities of constructors, if we're going to bring in the capability of private constructors, then we must think of a way to let user create a public interface for constructing an instance from a class. The .new() makes this awkward, if the user cannot overwrite .new(), they'll have to define a different method instead:

class Foo
    public x: number
    private function __init(self, x: number)
        self.x = x
    end
    function create(x: number) -- We now have two very similar methods that are used to construct a new instance from a class, .create and .new
        local self = Foo.new(x) 
        return self
    end
end

If we did not reserve .new(), and switched to calling the class object, we achieve far simpler syntax, with more capability:

class Foo
    public x: number
    private function __init(self, x: number)
        self.x = x
    end
    function new(x: number)
        local self = Foo(x) 
        return self
    end
end
local newObj = Foo.new(1) -- ok
local newObj_2 = Foo(1) -- not ok

This allows us to achieve encapsulation in a way that doesn't introduce additional confusion nor complexity to the language.
Additionally, POD style classes can be constructed simpler like this:

class Foo
    public x: number
    public y: number
    public z: number
end
local newObj = Foo { x = 1, y = 2, z = 3 } -- Calls the default constructor with a simpler syntax

On another note, if someone wished to create a class that wraps around a certain primitive, let's say buffer, they could stay consistent with how buffers are normally constructed, by defining a .create method. It grants users the capability of designing classes however they wish.

Change the rules around constructor inheritation

The RFC defines class constructors as inheritable, which should absolutely not be allowed.
Consider this simple example:

class Foo
    public x: number
    function __init(self, x)
        self.x = x
    end
end
class Bar extends Foo
    public y: number
end
Bar(1)  -- How is y initialized?

The intention behind inheriting constructors is to not leave any field uninitialized, and yet, inheriting constructors does not solve this problem. It creates unpredictable behavior, and does not add any additional benefits. The RFC proposes updating strict and non-strict to create a warning to the user, but why not just prohibit it instead?

The problem of uninitialized fields in constructors within inheritance can be avoided by setting a new rule:

If a base class defines a constructor, then the child class must also define one. The child class can then call the parent class' constructor to initialize certain fields, or just do it manually. This prevents constructors from being inherited, while solving our uninitialized fields problem.


Classes should be thought about from the perspective of potential new access specifiers and commonly used features from other languages. It should not be overly strict, and we shouldn't define rules without thinking about the future. I'd like the RFCs for classes to be solid, and can be expanded on later, which unfortunately the previous RFCs failed to succeed in.

@greentheblaze

Copy link
Copy Markdown

@TenebrisNoctua If by not reserving .new you mean making it so that the parentheses syntax is the standard, I 100% agree. Internally it would make more sense for that to go on for the default constructor, especially for something like inheritance.

The only trade-off is that users could get confused between function call syntax and classes, however like you said, people can get used to it, or do what I'd do:

Personally, I would still define .new in my classes as the default constructor for the purposes of readability and consistency with existing libraries and APIs, but with your idea, this is still possible. 👍

…xplicit constructor when you derive from a class that defines one.
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.