From 6439719cd22bd7f762c4a04df726e8bea081b094 Mon Sep 17 00:00:00 2001 From: Alexander McCord <11488393+alexmccord@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:06:53 -0700 Subject: [PATCH] Facelift of the Account OO example. (#578) When the original version was written, bidirectional type inference didn't exist, so this new kind of pattern was simply not possible. Now that it does, we can stop telling users to use the original typeof workaround. The only thing that's a bit unfortunate is the cast of `{}` into `AccountImpl`, but it's a necessary evil for now until we get a few more big ticket items. When we have that, we can likely kill the cast and call it redundant. --- docs/_pages/typecheck.md | 54 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/_pages/typecheck.md b/docs/_pages/typecheck.md index 1a11406e..6c709bdd 100644 --- a/docs/_pages/typecheck.md +++ b/docs/_pages/typecheck.md @@ -419,9 +419,9 @@ type Callback = { f: (Args...) -> Rets... } type A = Callback<(number, string), ...number> ``` -## Typing idiomatic OOP +## Adding types for faux object oriented programs -One common pattern we see throughout Roblox is this OOP idiom. A downside with this pattern is that it does not automatically create a type binding for an instance of that class, so one has to write `type Account = typeof(Account.new("", 0))`. +One common pattern we see with existing Lua/Luau code is the following OO code. While Luau is capable of inferring a decent chunk of this code, it cannot pin down on the types of `self` when it spans multiple methods. ```lua local Account = {} @@ -435,16 +435,62 @@ function Account.new(name, balance) return setmetatable(self, Account) end +-- The `self` type is different from the type returned by `Account.new` function Account:deposit(credit) self.balance += credit end +-- The `self` type is different from the type returned by `Account.new` function Account:withdraw(debit) self.balance -= debit end -local account: Account = Account.new("Alexander", 500) - --^^^^^^^ not ok, 'Account' does not exist +local account = Account.new("Alexander", 500) +``` + +For example, the type of `Account.new` is `(name: a, balance: b) -> { ..., name: a, balance: b, ... }` (snipping out the metatable). For better or worse, this means you are allowed to call `Account.new(5, "hello")` as well as `Account.new({}, {})`. In this case, this is quite unfortunate, so your first attempt may be to add type annotations to the parameters `name` and `balance`. + +There's the next problem: the type of `self` is not shared across methods of `Account`, this is because you are allowed to explicitly opt for a different value to pass as `self` by writing `account.deposit(another_account, 50)`. As a result, the type of `Account:deposit` is `(self: { balance: a }, credit: b) -> ()`. Consequently, Luau cannot infer the result of the `+` operation from `a` and `b`, so a type error is reported. + +We can see there's a lot of problems happening here. This is a case where you will have to guide Luau, but using the power of top-down type inference you only need to do this in _exactly one_ place! + +```lua +type AccountImpl = { + __index: AccountImpl, + new: (name: string, balance: number) -> Account, + deposit: (self: Account, credit: number) -> (), + withdraw: (self: Account, debit: number) -> (), +} + +type Account = typeof(setmetatable({} :: { name: string, balance: number }, {} :: AccountImpl)) + +-- Only these two annotations are necessary +local Account: AccountImpl = {} :: AccountImpl +Account.__index = Account + +-- Using the knowledge of `Account`, we can take in information of the `new` type from `AccountImpl`, so: +-- Account.new :: (name: string, balance: number) -> Account +function Account.new(name, balance) + local self = {} + self.name = name + self.balance = balance + + return setmetatable(self, Account) +end + +-- Ditto: +-- Account:deposit :: (self: Account, credit: number) -> () +function Account:deposit(credit) + self.balance += credit +end + +-- Ditto: +-- Account:withdraw :: (self: Account, debit: number) -> () +function Account:withdraw(debit) + self.balance -= debit +end + +local account = Account.new("Alexander", 500) ``` ## Tagged unions