-
Notifications
You must be signed in to change notification settings - Fork 89
Classes!! #191
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
base: master
Are you sure you want to change the base?
Classes!! #191
Changes from 10 commits
f584e90
9a65cd3
9e17a74
abaa555
f65f47f
483287a
7ae771b
3a00473
35238f1
d0d9dc0
cfabb49
77fd475
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,276 @@ | ||
| # Classes | ||
|
|
||
| ## Summary | ||
|
|
||
| ```luau | ||
| class Point | ||
| public x: number | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way for me to define a static member such as
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not yet. It's a good idea for an extension though. |
||
| public y | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can class fields have default values?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not yet. Sounds like a nice extension though.
Comment on lines
+7
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we do the crazy thing and said that fields need commas or semicolons? Then the bare fields like these just doesn't look so strange after all. So for this class: class Point
x: number,
y,
public function new(x, y)
return Point { x = x, y = y }
end
public function components(self)
return self.x, self.y
end
public function length(self)
local x, y = self:components()
return math.sqrt(x*x + y*y)
end
public function __add(self, rhs: Point)
local lx, ly = self:components()
local rx, ry = self:components()
return Point.new(lx+rx, ly+ry)
end
end |
||
|
|
||
| function length(self) | ||
| return math.sqrt(self.x * self.x + self.y * self.y) | ||
| end | ||
|
|
||
| function __add(self, other: Point) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to do the C++ thing and say identifiers in classes with two leading underscores are not valid methods, fields, nor are they metamethods, except for all existing metamethods that makes sense for an
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would have to exclude
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I had been leaning "no," but it would be a nice way to catch some simple errors. (eg writing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For me, it was about ensuring forward compatibility with any new metamethods Luau may have in the future. I think because of that, I'd rather flip from a denylist to an allowlist? |
||
| return Point { x = self.x + other.x, y = self.y + other.y } | ||
| end | ||
|
|
||
| function __tostring(self) | ||
|
andyfriesen marked this conversation as resolved.
|
||
| return `Point \{ x = {self.x}, y = {self.y} \}` | ||
| end | ||
|
|
||
| function new(x, y) | ||
| return Point { x = x, y = y } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the constructor private to the class or to the module, or is it always public? Or is it the Rust rule: if all fields are public, the constructor is also public?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Rust rule. |
||
| end | ||
| end | ||
|
|
||
| local p = Point.new(3, 4) | ||
| print(`Check out my cool point: {p} length = {p:length()}`) | ||
| ``` | ||
|
|
||
| ## Motivation | ||
|
|
||
| * People write object-oriented code. We should afford it in a polished way. | ||
| * Accurate type inference of `setmetatable` has proven to be very difficult to get right. Because of this, the quality of our autocomplete isn't what it could be. | ||
| * A construct with a fixed shape and a completely locked-down metatable will open up optimization opportunities that could improve performance: | ||
| * If classes can only be declared at the top scope, then we know that each method of each class has exactly one instance. This makes it simple for the compiler to know the exact function that will be invoked for any method call expression. | ||
| * If a value is known to be an instance of a particular class, the bytecode compiler should be able optimize method calls to skip the whole `__index` metamethod process and instead generate code to directly call the correct method. | ||
| * By the same token, method calls can be inlined more aggressively. Particularly self-method calls eg `self:SomeOtherMethod()` | ||
| * Field accesses can compile to a simple integral table offset so that the VM doesn't need to do a hashtable lookup as the program runs. | ||
| * Since every instance of a class has the same set of properties, we can split the hash table: The set of fields can be associated with the class and instances only need to carry the values of those fields. We think this can improve performance by improving cache locality. | ||
|
|
||
| ## Design | ||
|
|
||
| ### Syntax | ||
|
|
||
| Class definitions are a block construct. They can only be written at the topmost scope. `export class X` is allowed. | ||
|
|
||
| Defining two classes with the same name in the same module is forbidden. | ||
|
|
||
| Within a class block, two declarations are allowed: Fields and methods. | ||
|
|
||
| Fields are introduced with the new `public` keyword. We also plan to eventually offer `private`, but is sufficiently complex that it merits its own RFC. | ||
|
|
||
| Methods are introduced with the familiar `function` keyword. `public function f()` is also permitted. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right now, all |
||
|
|
||
| If a method's first argument is named `self`, it can be invoked with the familiar `instance:method()` call syntax. Type annotations on the `self` parameter are not allowed. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implies that every function in the class whose first argument is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You shouldn't be able to call a method that takes
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've put a ton of thought into this kind of thing. If we walk down this path, it will be a more general typechecking mode where all annotations are validated at runtime rather than having one magic special case just for the type of For this iteration, the type of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's kind of disappointing, I think, because that means the compiler has to generate pessimistic branch for |
||
|
|
||
| If a method accepts no arguments, or if its first argument is not named `self`, it can be invoked via `Class.method()` syntax. This is the same as "static methods" from other languages. | ||
|
|
||
| To create a new instance of a class, invoke it as if it were a function. It accepts one argument: A table that describes the initial values of all its properties. If more customization is desired, static factory functions (frequently named `new()` or `create()`) are an easy, familiar way to accomplish this. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does anything happen if the table has excess properties that aren't present in the class?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd really like for that to be an error.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this should error too. Just to be explicit about it. local t = setmetatable({ x = 1 }, {
__index = function(self, k)
return if k == "y" then 2 else nil
end
})
class Point
x: number,
y: number,
end
local pt = Point(t)The question is whether this should also error, though: local t = setmetatable({ x = 1, y = 2 }, { --[[ any metatables at all ]] })
class Point
x: number,
y: number,
end
local pt = Point(t) |
||
|
|
||
| Classes can define familiar Luau metamethods like `__add` and `__sub`. They will work as one would expect. | ||
|
|
||
| The following metaproperties are forbidden. Any attempt to define them is a syntax error: | ||
|
|
||
| * `__index` | ||
| * `__newindex` | ||
|
Comment on lines
+66
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's potentially something interesting we could do here? This could end up having the same semantics as This would unlock a really nice API for some
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The side effect of this is that you can't write function get_component(p: Point, c: "x" | "y")
return p[c]
end
local x = get_component(point, "x")
local y = get_component(point, "y")But that's ok, I think? You can just write this: function get_component(p: Point, c: "x" | "y")
return if c == "x" then p.x
else if c == "y" then p.y
else absurd(c)
end
local x = get_component(point, "x")
local y = get_component(point, "y") |
||
| * `__mode` | ||
| * `__metatable` | ||
| * `__type` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to reserve all method names starting with a double underscore ( |
||
|
|
||
| #### Class Objects | ||
|
|
||
| The action of evaluating a class definition statement introduces a *class object* in the module scope. A class object is a value that serves as a factory for instances of the class and as a namespace for any functions that are defined on the class. | ||
|
|
||
| Class objects behave like class instances in most ways, but are always `const` and frozen. | ||
|
|
||
| Taking references to class methods via `ClassName.method` syntax is allowed so that classes can easily compose with existing popular APIs: | ||
|
|
||
| ```luau | ||
| local n = pcall(SomeClass.getName, someClassInstance) | ||
| ``` | ||
|
|
||
| To construct an instance of a class, call the class object as though it were a function. It accepts a single argument: a table that contains initial values for all the fields. | ||
|
|
||
| The top type of all class objects is named `class`. `type()` and `typeof()` return `"class"` when passed a class object. | ||
|
|
||
| #### Class Instances | ||
|
|
||
| Class instances are a new type of value in the VM. They are similar but not quite the same as tables. They have no array part, for instance. | ||
|
|
||
| `pairs`, `ipairs` , `getmetatable`, and `setmetatable` do not work on class instances. They also cannot be iterated over with the generic `for` loop. (unless the class implements `__iter`) | ||
|
|
||
| Reading or writing a nonexistent class property raises an exception. This makes it easy to disambiguate between a nonexistent property and a property whose value is nil. | ||
|
|
||
| The builtin `type()` and `typeof()` functions return `"object"` for any class instance. We chose this over having them return the class name because class names do not have to be globally unique (they must only unique within a single module) and because we do not want to make it possible for classes to impersonate other types. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've always interpreted
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main problem I want to really address is that Class names aren't required to be globally unique, so you could get wedged into a bad situation if a class happens to be unfortunately named.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to avoid code where I am mixing Additionally, it can be nice to use these methods to 'inspect' the type. With
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is incorrect, neither
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, let's be honest. I don't think the code in the example is particularly great. Just write the |
||
|
|
||
| ```luau | ||
| class Cls end | ||
| local inst = Cls {} | ||
|
|
||
| type(Cls) == "class" | ||
| typeof(Cls) == "class" | ||
|
|
||
| type(inst) == "object" | ||
| typeof(inst) == "object" | ||
| ``` | ||
|
|
||
| Comparisons between object instances is the same as with tables: If `__eq` is not defined, object comparisons use physical (pointer) equality. `__eq` is only invoked if both operands are the same type. | ||
|
|
||
| #### The `class` library | ||
|
|
||
| We introduce a new global library `class`. Its contents are | ||
|
|
||
| ```luau | ||
| local class: { | ||
| isinstance: (o: unknown, C: class) -> boolean, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's take this one out? It's backward compatible to retrofit the
This would mean the compiler can avoid generating the pessimistic branch where export function foo(o: MyClass)
if not o is MyClass then -- maybe we should add `o is not MyClass`?
-- adding a `return` here to prevent a join in the codegen's CFG
return error(`expected {MyClass} but got {typeof(o)}`)
end
-- now the compiler knows it can generate specialized code for `o`
return o.foo
end
export class MyClass
foo: number,
bar: number,
end |
||
| classof: (o: unknown) -> class?, | ||
|
andyfriesen marked this conversation as resolved.
|
||
| } | ||
| ``` | ||
|
|
||
| This library also serves as an obvious extension point for future features like reflection. | ||
|
|
||
| The function `class.isinstance(o, Class)` returns `true` if the object `o` is an instance of `Class`. At runtime, it raises an exception if the second argument is not a class object. If the first argument is not a class instance, `class.isinstance` returns false. (eg `class.isinstance(5, MyClass)`) | ||
|
|
||
| ### Type System | ||
|
|
||
| Class definitions also introduce a new type to the type environment. | ||
|
|
||
| Unlike tables, which are structurally typed, class types are nominal. Two different classes with identical fields are treated as distinct types. | ||
|
|
||
| Inferring the types of class fields is fraught with difficulty, so un-annotated fields are given the type `any`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| The type introduced by a class definition is available anywhere in the source file. | ||
|
|
||
| The `class.isinstance` function participates in refinement: | ||
|
|
||
| ```luau | ||
| function foo(p: unknown) | ||
| if class.isinstance(p, Point) then | ||
| return {p.x, p.y} -- no error here | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| Each class object is a singleton instance of an unnamed type. If needed, it is easy to access via `typeof(TheClass)`. Class object types are all subtypes of the top `class` type. We choose this name to make it clear that it is not the top type of class instances. | ||
|
|
||
| ### Semantics | ||
|
|
||
| Class definitions are Luau statements just like function definitions. | ||
|
|
||
| The action of a class definition statement is to allocate the class object, define its functions and properties, and freeze it. Consequently, a class cannot be instantiated before this statement is executed. | ||
|
|
||
| We do, however, *hoist* the class identifier's binding to the top of the script so that it can be referred to within functions or classes that lexically appear before the class definition. This makes it easy and straightforward for developers to write classes or functions that mutually refer to one another. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Class hoisting implies the following code is valid: local function getFoo()
local f = Multiple.new(2)
return f
end
print(getFoo()) -- ???
local function getData(factor: number)
return 123 * factor
end
class Multiple
public factor: number
function length(self)
return getData(factor)
end
endWhat is the expected behavior where getFoo() is called? A runtime error?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. It's a runtime error.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am completely against introducing any hoisting to the language, with this case being a perfect example of why. If someone needs to reference a class before it is defined, they should use a forward-declared variable as a surrogate to explicitly show that they are using a token that may or may not be defined yet.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, the presence of hoisting seems to be part of the reason why class definitions aren't allowed outside the top scope, since hoisting scoped variables can get messy. I would wager everyone would prefer to have scoped classes over class hoisting if they had to choose between the two.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without hoisting, there is no way at all to write two classes that mutually refer to one another. The top-level constraint is about establishing that there is exactly on instance of every method of every class. This makes it significantly easier for us to do static method dispatch. I'll update the RFC to clarify this. Thanks!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's nothing wrong here. Classes as a statement has zero code to execute outside of the initialization. Fields do not have an initializer. Just don't let functions or locals reference any classes that are defined later. local function foo()
return Foo.new()
-- ^^^ `Foo` is not bound in scope here
end
class Foo -- now `Foo` is bound in scope
bar: Bar
function new()
return Foo { bar = Bar { x = 5 } }
end
end
print(Foo.new().bar.x) -- prints 5. It's fine.
class Bar -- now `Bar` is bound in scope
x: number
end
-- ...the rest of the module...The right way to do the hoisting is by doing something like this: class <unutterable-1>
bar: <unutterable-2>
function new()
return <unutterable-1> { bar = <unutterable-2> { x = 5 } }
end
end
class <unutterable-2>
x: number
end
local function foo()
return Foo.new()
-- ^^^ `Foo` is not bound in scope here
end
const Foo = <unutterable-1>
print(Foo.new().bar.x) -- prints 5. It's fine.
const Bar = <unutterable-2>That is, what you do is group all classes into a mutually dependent block that can reference each other unconditionally, and then and only then do you incrementally bring the name of the individual class into scope. The reason why hoisting in JS gets so much hate is because it relies on TDZ which cannot be analyzed at compile time, that's literally why TDZ exists at runtime. Here, we can warn when attempting to reference
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That said, the RFC as is does phrase it like the solution is either-or: 1. no way for two classes to be defined out-of-order or mutually dependent in some way, or 2. do TDZ and say all class identifiers are hoisted to the top. The solution I suggested avoids this problem, as long as classes do not have any initializers.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sigh. Flip the direction. Functions in class definitions can capture locals. TDZ. class Bar
function run_foo()
Foo.print_x()
end
end
Bar.run_foo()
local x = 5
class Foo
function print_x()
print(x)
end
end
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What you're describing is an explicit phrasing of exactly what the RFC proposes except that we lose the It's not the best but I don't see a viable alternative. I also don't know if it's really all that bad: It only arises when code is interleaving class definitions and executing imperative actions at the module scope.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...So class variables are effectively globals? If that's the case, how do shadowing and global variables behave with class definitions? local A = 1 -- shadows A?
print(A) -- 1 or nil?
class A end
print(A) -- class A?
B = 2 -- global variable or parse error from reassignment?
print(B) -- 2 or nil?
class B end
print(getfenv()["B"]) -- 2?Or what if the module returns early before a class is initialized? class A
function f()
return B {} -- 50/50 chance always nil?
end
end
if math.random(0, 1) == 1 then return end
class B endEven if a set of consistent rules can be made, it just seems like a lot of footguns are going to be caused by this. |
||
|
|
||
| Static analysis also considers the class's type to be global to the whole module so that it can appear in any type annotation anywhere in the script. | ||
|
|
||
| An example: | ||
|
|
||
| ```luau | ||
| -- illegal: MyClass is not yet defined | ||
| local a = MyClass {} | ||
|
|
||
| -- OK: MyClass can appear in any type annotation anywhere | ||
| function use(c: MyClass) | ||
| end | ||
|
|
||
| function create() | ||
| -- OK as long as this function is invoked after the class definition statement | ||
| return MyClass {} | ||
| end | ||
|
|
||
| -- We can't statically catch this in the general case, but this will fail at runtime! | ||
| create() | ||
|
|
||
| class MyClass | ||
| end | ||
|
|
||
| local b = MyClass {} -- OK | ||
| local c = create() -- OK | ||
| ``` | ||
|
|
||
| Because class definition is a statement, class methods can capture upvalues just like ordinary functions do. | ||
|
|
||
| ```luau | ||
| local globalCount = 0 | ||
|
|
||
| class Counter | ||
| public count: number | ||
|
|
||
| function new() | ||
| local count = globalCount | ||
| globalCount += 1 | ||
| return Counter {count=count} | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| ### Out of scope for now | ||
|
|
||
| #### Private fields, const fields | ||
|
|
||
| These are things we want to do, but integrating them with the existing structural type system is surprisingly tricky and will be tackled in separate RFCs. | ||
|
|
||
| #### Generic classes | ||
|
|
||
| We're very excited to support generic classes and plan to introduce a fresh RFC to deal with them specifically. | ||
|
|
||
| #### Inheritance | ||
|
|
||
| We're still evaluating whether or not implementation inheritance is something we want to support. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems to not be possible to me. Suppose: There's nothing you can do to populate the field This tells me that inheritance is pretty much impossible to implement in a way that is actually sound in Luau. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this not a problem simply solvable with types? I would expect
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shadowing is certainly a huge problem that we'd have to solve. Inheritance is complex enough that it merits an RFC all by itself. |
||
|
|
||
| Method inlining is something we'd like to try that is greatly complicated by inheritance. | ||
|
|
||
| Also, frankly, its worth as a programming technique is controversial: the [Fragile Base Class Problem](https://en.wikipedia.org/wiki/Fragile_base_class) can cause significant harm to a project. | ||
|
|
||
| Lastly, Luau easily supports interface inheritance through its structural type system, so inheritance is judged to be lower priority. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Luau does not support interface inheritance through the structural type system for any type that has methods because method types are not expressible in the type system. Examples: type interface = { foo: (self: interface) -> () }
type something = { value: number, foo: (self: something) -> () }
local function fn(obj: interface)
obj:foo()
end
local function create(): something
return {
value = 0,
foo = function(self: something)
print(self.value)
end,
}
end
fn(create()) -- type error!type interface = { foo: <self>(self: self) -> () }
type something = { value: number, foo: (self: something) -> () }
local function fn(obj: interface)
obj:foo()
end
local function create(): something
return {
value = 42,
foo = function(self: something)
print(self.value)
end,
}
end
fn(create()) -- type error!type interface = { foo: <self>(self: self) -> () }
local function fn(obj1: interface, obj2: interface)
obj1.foo(obj2) -- runtime error!
endThere are other avenues, but I believe I have explored them all, and it seems that there is no way to express interfaces with methods in Luau in a way where the type system gives no false positives and where it also catches common runtime errors. And this is without getting into generic interfaces. |
||
|
|
||
| ## Drawbacks | ||
|
|
||
| This is a really big feature that has lots of moving parts! | ||
|
|
||
| We need to introduce multiple new contextual keywords: `class` and `public` to start and `private` later. We also introduce at least one new top type `class`. (we probably also need a corresponding `object` top type for class instances) | ||
|
|
||
| Allowing code to grab unbound method references (ie `local m = o.someMethod`) seems risky because it opens the doorway to a lot of difficult-to-optimize dynamism, but it also makes a bunch of nice things like `pcall` work exactly the way developers expect. We're making the bet here that this does not materially affect our ability to optimize more mundane attribute access or method calls. | ||
|
|
||
| The word `class` is doing triple duty under this RFC: It is a contextual keyword, the name of a top-level library, and the name of the top type for class objects. | ||
|
|
||
| Object oriented codebases tend to have far more cyclic dependencies between modules because every piece of data is also coupled to a whole bunch of functions that operate on that data. We are probably going to have to work out a way to relax the restrictions on cyclic module imports. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| [Arseny's record proposal](https://github.com/luau-lang/luau/blob/7f790d3910bbfc2adf007da3551b0a13e42ebb7a/rfcs/records.md). This proposal is really quite similar, but looks a bit more familiar to users coming from other languages and affords the development of features like private fields. | ||
|
|
||
| [Shared Self Types](https://github.com/luau-lang/rfcs/blob/master/docs/shared-self-types.md). This proposal was intended to shore up table type inference in the case that the code was written in an OO style, but after significant work, it doesn't actually work all that well in practice. The resulting system was very brittle and tricky to work with. Trickier, in fact, than the pattern that developers are already writing today. | ||
|
|
||
| ### Field syntax | ||
|
|
||
| We considered a number of possibilities for field syntax before settling on `public` and `private`. | ||
|
|
||
| Type annotations must be optional, so the syntax we choose must work well in that situation. | ||
|
|
||
| We considered using the existing `local` keyword for class fields, but judged it to be a bridge too far: Fields are not locals! They do not inhabit the stack. | ||
|
|
||
| We also considered using no keyword at all and judged that to be unacceptable in the case that a field also has no type annotation. A single bare identifier on a line all by itself looks too weird and might make the grammar difficult to change later on. | ||
|
|
||
| So there must be a keyword and it cannot be `local`. Other keywords we considered were `var`, `let`, and `field`. | ||
|
|
||
| ```luau | ||
| class Test | ||
| local foo | ||
|
|
||
| public foo | ||
| public foo2: number | ||
|
|
||
| private local foo2: number | ||
| private foo3: number | ||
|
|
||
| var bar | ||
| var bar2: number | ||
|
|
||
| field bar | ||
| field bar2: number | ||
|
|
||
| quux | ||
| quux2: number | ||
| end | ||
| ``` | ||
|
|
||
| `let` is a great keyword to introduce a local binding, but does not make a ton of sense in a class declaration. (especially a declaration where we’re not providing an initializer!) | ||
|
|
||
| `var` is pretty good and also makes sense as a token to introduce a local, but is largely redundant. | ||
|
|
||
| `field` is spiritually aligned with the syntax sensibilities that drove the development of Lua, but has no precedent in any other language. | ||
|
|
||
| `public` and `private` work pretty well from a parsing perspective, have no historical precedent in Lua, and also encode ideas that we definitely want classes to support. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to be able to write
class MyClass: SomeSignature & SomeOtherSignature ... endas a way to make sureMyClasssatisfies these requirements (or to unify with any metavariables in the class fields/methods).