Add syntactic sugar for OOP in Luau

As a Roblox developer, it is currently too hard to use object oriented programming without having to worry about low level stuff (especially with Luau type checking) and make my code look clean.

I know, true OOP isn’t coming to Lua or Roblox anytime soon but good feature would be syntactical sugar for classes which would use metatables under the hood.

Why?

  • Read this post which explains all of the nightmares you’ll get when you try to use OOP with Luau type checking such as painful types like {__meta: {GetMember: ({-member: free6192-1-0-}) -> free6192-1-0, __index: <CYCLE>, new: (string) -> ({__meta: {GetMember: ({- member: free6190-2-1 -}) -> free6190-2-1, __index: <CYCLE>, new: (string) -> (<CYCLE>, free6193-1-0) }, member: string}, free6193-1-0)}, member: string}.
  • As @Shedletsky and @Elttob mentioned in that topic, classes made using my proposed syntactical sugar improve readability. I would personally prefer syntactical sugar for classes over the current way of writing classes.
  • As @davness said in that topic, currently you have to deal with low-level stuff like setting __index (also types when Luau comes out). Developers should focus on the class, it’s methods and properties instead of the low-level stuff. Those low-level things could as well be handled by Roblox under the hood.

How?
In that topic, I proposed a simple, clean way of writing classes. I came up with ideas and here they are:

class Player
	username = "Username" -- Class property
	hair = "None"
	
	constructor(playerUsername) -- Class constructor
		self.username = playerUsername
	end
	
	function ShowUsername() -- Class method
		print(self.username)
	end
end

class BaconhairPlayer << Player -- Inheritance
	static hair = "Pal Hair" -- No override keyword needed & static keyword can be used for making properties or methods static
	static username = "UseStarCode_BACON"
end

print(BaconhairPlayer.hair)
print(BaconhairPlayer.username)

local devKittenzPlayer = new Player("Dev_Kittenz")
devKittenzPlayer:ShowUsername()
print(devKittenzPlayer.hair)

With Luau, the Player class would look something like this:

class Player
	username = "Username": string -- Class property
	hair = "None": string
	
	constructor(playerUsername) -- Class constructor
		self.username = playerUsername
	end
	
	function ShowUsername(): nil -- Class method
		print(self.username)
	end
end

-- No "type Player = typeof(new Player())" is required, Roblox automatically creates one

Those new keywords could be introduced as context-sensitive ones so they would be fully backwards-compatible.

If Roblox is able to address this issue, it would improve my (and most likely many other devs) development experience because then we wouldn’t have to worry about low-level stuff when writing classes, types would be handled by Roblox under the hood and our code could look cleaner and easier to read.

21 Likes

No offense, but you can avoid this entire problem if you didn’t use OOP. Since there is no native support for OOP in Lua, ofc it’s gonna be a pain to implement.

1 Like

OOP is a popular and widely used tool in many top Roblox games today, so it would be dangerously ignorant to dismiss it entirely, no matter what you think of it personally.

Also, a suggestion for the syntax; in line with the syntax for typed Luau, perhaps the inheritance ‘operator’ could be a left arrow <- or colon :?

9 Likes

I would make it ← because : is used for types

Neither would work. <- would have to be disambiguated

1 Like

The inheritance symbol can only appear after a class statement, and so there would be no ambiguity as those maths operators don’t ever appear at the start of a Lua statement, and therefore can never be directly after the start of a class block.

Consider this snippet:

class Foo <-

In this case, the only token that could follow is an identifier.

Now, consider this:

class Foo

The only tokens that could follow here are an identifier, a comment, a keyword or parentheses. No relational infix operators can ever appear immediately after the identifier token representing the class name.

2 Likes

There is ambiguity though because it would still be “tokenized” as a single token. The “tokenizer” does not care. It sees it as one token.

This issue can come from something like this:

local a = b <- c

Now it is an unexpected token.

Oh and consider this too

local
class foo <- bar

Lua(u)'s grammar for the most part is context-free. This little change would complicate things further.

Note the “for the most part” for Luau. type and continue were special cases to being added.


At the end, this feature request is unfortunately infeasible and the engineering efforts required for a niche case, don’t seem worth it.

1 Like

I’m assuming you didn’t notice how, in my first reply, I used the term ‘operator’ in quotes.

The inheritance symbol would in reality not be represented as one token, most likely. It’s just as sufficient to consider it as two separate symbol tokens without any whitespace tokens between them, and completely avoids that issue.

Anyway, getting back to the main topic: I think that the benefit and standardisation of a widespread and hard to type check coding pattern is well worth the engineering cost to implement it. The addition of the continue pseudo-keyword is pretty solid proof that Luau is capable of handling those kinds of contextual keywords.

Outside of detecting the class pseudo-keyword, there’s actually not a whole lot of contextual stuff going on here. It’s pretty basic token matching for the areas you highlighted.

I would make all of those new keywords contextual because of backwards compatibility.

Also, ← would be better because colon might make some people think it’s a type declaration when in reality it is inheritance.

So the bacon hair class would look like this:

class BaconhairPlayer <- Player -- Inheritance
	static hair = "Pal Hair" -- No override keyword needed & static keyword can be used for making properties or methods static
	static username = "UseStarCode_BACON"
end
2 Likes

Out of curiosity, I wanted to see how this holds up with a proper class:

Metatable based approach

local Maid = {__index = {}}

function Maid.new()
    return setmetatable({tasks = {}}, {__index = Maid.__index})
end

function Maid:add(task)
    table.insert(self.tasks, task)
end

function Maid:clean()
    for index=#self.tasks, 1, -1 do
        local task = self.tasks[index]
        local taskType = typeof(task)

        if taskType == "function" then
            task()
        elseif taskType == "Instance" then
            task:Destroy()
        elseif taskType == "RBXScriptConnection" then
            task:Disconnect()
        end
    end
    self.tasks = {}
end

return Maid

Proposed new syntax

class Maid

    constructor()
        self.tasks = {}
    end
    
    function add(task)
        table.insert(self.tasks, task)
    end
    
    function clean()
        for index=#self.tasks, 1, -1 do
            local task = self.tasks[index]
            local taskType = typeof(task)
    
            if taskType == "function" then
                task()
            elseif taskType == "Instance" then
                task:Destroy()
            elseif taskType == "RBXScriptConnection" then
                task:Disconnect()
            end
        end
        self.tasks = {}
    end
end

return Maid
1 Like

I would say the second one looks cleaner to read (one reason why I want this to be a feature on Roblox).

2 Likes

I don’t know if this was in the Q&A or not (which weren’t recorded), but I distinctly remember @zeuxcg mentioning plans for this in his talk at RDC 2019.

11 Likes