-
Notifications
You must be signed in to change notification settings - Fork 112
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
class support in Teal #861
Comments
I suppose this is a duplicate of #97. I decided to try replicating your example using pure Teal (on 0.24.1) using the game/entity.tl -- we need to make an interface that can be inherited by
-- records and interfaces
local interface IEntity
x: number
y: number
-- we must write methods signatures too
move: function(self, number, number)
dash: function(self)
end
-- since we can't make method definitions on interfaces, we must use a record for that, which
-- will be returned at the end, thus to export the interface, we make an alias here
local record Entity is IEntity
-- I found Entity.IEntity pretty redundant to write
type Interface = IEntity
end
-- for convenience (and optimization), write the Entity metatable here
local entity_mt: metatable<Entity> = {
__index = Entity
}
-- for initialization, init does the trick, it initializes "an IEntity" (not "the Entity")
function Entity.init(self: IEntity, x: number, y: number)
self.x = x
self.y = y
end
-- 100% optional for this example, but I like doing this
function Entity.new(x: number, y: number): Entity
local entity: Entity = setmetatable({}, entity_mt)
entity:init(x, y)
return entity
end
-- methods! defined at the Entity record
function Entity:move(dx: number, dy: number)
self.x = self.x + dx
self.y = self.y + dy
end
function Entity:dash()
self:move(50, 0)
end
-- note that you can also return interfaces as a module too, but since we
-- can't define methods on interfaces (since they are abstract, that is, not concrete like records),
-- we export the Entity record, with the Interface alias inside
return Entity game/enemy.tl -- require the Entity record, if this was the interface, then
-- it would be `local type` instead of just `local`
local Entity = require 'game.entity'
-- same thing, we define the interface that can be inherited, then the record;
-- note how IEnemy inherits Entity's interface;
-- btw, types can also be defined inside interfaces;
local interface IEnemy is Entity.Interface
enum Kind
'angry'
'faster'
end
kind: Kind
end
local record Enemy is IEnemy
type Interface = IEnemy
end
-- By applying Entity on Enemy's __index metafield directly, inheritance works
-- at run-time, because Enemy values will have it's __index to Enemy,
-- which does have an __index to Entity, in other words:
-- `value: Enemy` --> `Enemy` --> `Entity`
setmetatable(Enemy, { __index = Entity })
local enemy_mt: metatable<Enemy> = {
__index = Enemy
}
-- Same idea, re-using original Entity.init
function Enemy.init(self: IEnemy, x: number, y: integer, kind: Enemy.Kind)
Entity.init(self, x, y)
self.kind = kind
end
function Enemy.new(x: number, y: integer, kind: Enemy.Kind): Enemy
local enemy: Enemy = setmetatable({}, enemy_mt)
enemy:init(x, y, kind)
return enemy
end
-- override Enemy.dash by defining dash here, this way
-- enemy_value:dash() will access Enemy.dash instead of Entity.dash
function Enemy:dash()
-- in this case, Enemy.dash does double dash if it's the 'faster' kind
-- using the `as` keyword for base methods are necessary
Entity.dash(self as Entity)
if self.kind == 'faster' then
Entity.dash(self as Entity)
end
end
return Enemy game/monster.tl -- Same concepts here :)
local Enemy = require 'game.enemy'
local interface IMonster is Enemy.Interface
name: string
health: integer
hit: function(self, integer)
end
local record Monster is IMonster
type Interface = IMonster
end
setmetatable(Monster, { __index = Enemy })
-- private methods are just local functions with a self parameter
local function monster_tostring(self: Monster): string
return string.format(
"Monster { x = %d, y = %s, kind = '%s', name = '%s', health = %d }",
self.x, self.y, self.kind, self.name, self.health
)
end
local monster_mt: metatable<Monster> = {
__index = Monster,
__tostring = monster_tostring,
}
function Monster.init(self: IMonster, x: number, y: integer, name: string, kind?: Enemy.Kind)
Enemy.init(self, x, y, kind or 'angry')
self.name = name
self.health = 10
end
function Monster:hit(damage: integer)
self.health = self.health - damage
end
function Monster.new(x: number, y: integer, name: string, kind?: Enemy.Kind): Monster
local monster: Monster = setmetatable({}, monster_mt)
monster:init(x, y, name, kind)
return monster
end
return Monster test.tl -- Let's test!
-- require the Monster
local Monster = require 'game.monster'
-- create a new monster, with a convenient new function :)
local monster = Monster.new(10, 20, 'Bob the monster')
-- move and hit, the typechecker accepts these at compile-time because they're
-- defined on the interfaces, and bound at run-time thanks to __index metafields
monster:move(10, 20) -- from Entity.move
monster:hit(3) -- from Monster.hit
print(monster)
monster:dash() -- from Enemy.dash instead of Entity.dash
print(monster)
local faster_monster = Monster.new(0, 0, 'Billy the fast monster', 'faster')
faster_monster:dash() -- double dash from 0 to 100 on x field
print(faster_monster) Output:
And that's it! Maybe an utility library could be made with generics to make things simpler, but this way both inheritance and method overriding works. |
By the way, @hishamhm, is this
|
@Andre-LA Sounds great, how would it be with a library? Using generics e.g. |
It would be great if Teal had built-in support for classes. I know it might be a bit unconventional, but creating classes in Teal with its type-checking system is quite challenging. I often struggle when trying to create classes using external libraries. Here's an example in Lua:
Adding type annotations to this code would be difficult. Introducing a keyword like class could make this process much easier. For example, in Teal, it could look like this:
This feature would simplify class creation and provide more robust type checking for classes.
Another issue arises when dealing with class inheritance. Many Lua libraries support inheritance through constructors or arguments, such as in the following example:
Here, Monster inherits properties and behaviors from both Entity and Enemy. Representing this in Teal is quite cumbersome because it involves carefully managing the types of the parent classes, ensuring proper overrides, and handling shared functionality. This often leads to verbose and complex type annotations, making the code harder to maintain and debug.
With a dedicated class keyword or built-in class system, inheritance could be implemented more intuitively. For example, the equivalent in Teal might look like this:
This syntax would be much cleaner and easier to understand, while still allowing Teal to enforce type safety. It would also help developers better express relationships between classes, such as parent-child hierarchies or multiple inheritance.
I apologize if this seems like a silly request. If there's an easier or better way to achieve this with the current version of Teal, I’d be grateful if you could provide guidance or examples.
The text was updated successfully, but these errors were encountered: