糖果实验室杂货铺

Candy Lab

深入探究MoonScript的类实现

1 year ago 0

prints "Player"


Two metafields are provided on the class objects metatable: __index and __call.


meta字段(metafiels)被提供在class对象的meta表中(metatable)。

The __call function is what is called when we create a new instance: Player() It’s responsible for creating a new table to be the instance, providing it with a metatable, then calling the constructor.

__call函数会在我们创建一个新实例时被调用:Player()的职责是把一个新表给成一个实例,提供一个meta表(metatable), 当我们调用构造函数时。


You can can see how the _base_0 is used directly as the metatable of the object.

你可看到_base_0是如何被用作对象的meta表的。(metatable)


Additionally, the class object has an __index metafield set to the base. This has a lot of implications. The most important is you can access any fields from base directly on the class object, assuming they haven’t been shadowed by any fields directly on the class object.

The base object


local _base_0 = {
say_hello = function(self)
return print("Greetings! I'm at " .. tostring(self.x) .. ", " .. tostring(self.y))
end
}
_base_0.__index = _base_0
_base_0.__class = _class_0
```

The base object, __base_0 is a regular Lua table. It holds all the instance methods of the class. Our example from above implemented a say_hello method which is compiled directly into the base.

The base object has a circular reference to itself in the __index field.

This lets us use the base object directly as the metatable of instances. The __index property is where instance methods are fetched from. Since it points to itself, the instance methods can be pulled directly from the metatable without any indirection.

Likewise, this also lets us implement other metamethods directly as instance methods of the class. I'll have an example below.

It’s a very cool concept, and definitely worth taking a moment to understand.

Lastly, a reference to the class placed on the base object with the name __class. This is how the @@ operator accesses the class object.

Classes with inheritance

Super invocation has changed a bit in MoonScript 0.4.0
Classes that inherit from other classes in MoonScript introduce a few more ideas. The extends keyword is used for inheritance:


class SizedPlayer extends Player
new: (@size, ...) => super ...

say_hello: => super!
print "I'm #{@size} tall"


Here’s the resulting Lua:


local SizedPlayer
do
local _parent_0 = Player
local _base_0 = {
say_hello = function(self)
_parent_0.say_hello(self)
return print("I'm " .. tostring(self.size) .. " tall")
end
}
_base_0.__index = _base_0
setmetatable(_base_0, _parent_0.__base)
local _class_0 = setmetatable({
__init = function(self, size, ...)
self.size = size
return _parent_0.__init(self, ...)
end,
__base = _base_0,
__name = "SizedPlayer",
__parent = _parent_0
}, {
__index = function(cls, name)
local val = rawget(_base_0, name)
if val == nil then
return _parent_0[name]
else
return val
end
end,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
if _parent_0.__inherited then
_parent_0.__inherited(_parent_0, _class_0)
end
SizedPlayer = _class_0
end

The majority of the generated code is the same as a regular class. Here are the differences:

local _parent_0 = Player
There’s a new local variable inside the do end block called _parent_0 that holds a reference to the parent class.


local _base_0 = {
-- ...
}
_base_0.__index = _base_0
setmetatable(_base_0, _parent_0.__base)


The metatable of the base is set to the base of the parent class. This establishes the inheritance chain for instances. If a method can’t be found on the class’s base, then the parent class’s base is automatically searched due to how __index works.

There’s a slight disadvantage to this. Metamethods are fetched with rawget, so metamethod inheritance does not work by default. We can work around this with the __inherited callback discussed below.


local _class_0 = setmetatable({
-- ...
__parent = _parent_0
}, {
-- ...
}


The parent class is stored on the class object in a field called __parent. This gives you an easy way to reference the parent class object.


{
__index = function(cls, name)
local val = rawget(_base_0, name)
if val == nil then
return _parent_0[name]
else
return val
end
end,
-- ...
}

The __index metafield on the class object is now a function, instead of a reference to the base (which is a table). rawget is used control the precedence of the properties. If the field can’t be found directly on the base then the parent class is searched.

Remember that class objects also pull fields from their bases, so this has the effect of searching both the parent class object and the parent class’s base. Even though we've used rawget on the base, we can still get access to the parent class’s base.


if _parent_0.__inherited then
_parent_0.__inherited(_parent_0, _class_0)
end


Lastly, we now have a class callback. When a subclass is created and the parent class has a method __inherited then it is called with the class object that has just been created.

The __inherited method works directly with class objects, no instances are involved.


local _base_0 = {
say_hello = function(self)
_parent_0.say_hello(self)
return print("I'm " .. tostring(self.size) .. " tall")
end
}

In the example I included a method that calls super. All MoonScript does is provide sugar for calling the method of the same name on the parent class.

Class tips and tricks

Now that you have an understanding of how a class in MoonScript is implemented, it’s easy to see how we can work with the internals to accomplish new things.

Adding __tostring and other metamethods

If you want your instances to have a string representation you can implement a __tostring method in the metatable.

As we saw above, the metatable has an __index field set to itself, we just need to implement metamethods as instance methods:


class Player
new: (@x, @y) => __tostring: => "Player(#{@x}, #{@y})"

print Player(2, 8) --> "Player(2, 8)"

All of Lua’s metamethods work (except __index, see below). Here’s an example of a vector class with overloaded operators:


class Vector
new: (@x, @y) => __tostring: => "Vector(#{@x}, #{@y})"

__add: (other) => Vector @x + other.x, @y + other.y

__sub: (other) => Vector @x - other.x, @y - other.y

__mul: (other) => if type(other) == "number"
-- scale
Vector @x * other, @y * other
else
-- dot product
Vector @x * other.x + @y * other.y

print Vector(1,2) * 5 + Vector(3,3) --> Vector(8, 13)


I mentioned above that metamethod inheritance does not work:


class Thing
__tostring: => "Thing"

class BetterThing extends Thing

print BetterThing! --> table: 0x1057290
We can work around this by using the __inherited callback:

class Thing
__tostring: => "Thing"
__inherited: (cls) => cls.__base.__tostring = @__tostring

class BetterThing extends Thing

print BetterThing! --> Thing


Adding a new method to a class after declaration

Now that we know about __base it’s easy to add new methods to classes that don’t have them.


class Player
new: (@name) =>-- add the new method
Player.__base.jump = => print "#{@name} is jumping!"

Player("Adam")\jump! --> Adam is jumping!


We can extend this concept even further to dynamically generate methods:


class Player
new: (@name) => for dir in *{"north", "west", "east", "south"}
@__base["go_#{dir}"]: => print "#{@name} is going #{dir}"

Player("Lee")\go_east! --> Lee is going east


Converting an existing table to an instance

Sometimes you might already have a table that you'd like to convert to an instance of a class without having to copy it. Now that we know how the __init method works we can use setmetatable to accomplish a similar result:


class Rect
area: => @w * @h

some_obj = { w: 15, h: 3 }

-- apply the metatable
setmetatable(some_obj, Rect.__base)

print some_obj\area! --> 45


This same method can be used to convert on object from type to another.

Adding __index metafield to an instance

MoonScript uses the __index metafield on class instances in order to allow instance properties to be looked up. If we just replace __inde with another implementation without any consideration we would break the instance. We'll have to chain our custom __index with the old one.

Here’s how we might implement getter methods:


class Thing
getters: {
age: => os.time! - @created_at
}

new: => @created_at = os.time!

mt = getmetatable @
old_index = mt.__index

mt.__index = (name) => if getter = old_index.getters[name]
getter @
else
if type(old_index) == "function"
old_index @, name
else
old_index[name]

t = Thing!
print t.age


Its’s important that you don’t try to access self (without rawget) within the __index metamethod, otherwise you'll cause an infinite loop.
Writing that massive implementation in the constructor isn’t ideal. Here’s a base class that automatically upgrades anyone who inherits with getter functionality:


class HasGetters
getters: {}
__inherited: (cls) => old_init = cls.__init
cls.__init = (...) => old_init @, ...

mt = getmetatable @
old_index = mt.__index

mt.__index = (name) => if getter = old_index.getters[name]
getter @
else
if type(old_index) == "function"
old_index @, name
else
old_index[name]

class BetterThing extends HasGetters
getters: {
age: => os.time! - @created_at
}

new: => @created_at = os.time!

t = BetterThing!
print t.age


The clever part here is replacing the __init method on the base class with a custom one that automatically injects support for getters.

特性增强(Future improvements)

The class system is far from perfect. Here are some future improvements that I'd like to add:

There’s no way to determine which order methods are added to a class. If you're going to be triggering side effects from method creation then your options are limited.
The MoonScript class meta-properties use double underscore just like Lua. If Lua ever decides to use any of the same names then there will be conflicts.
Closing

Not all of the functionality of MoonScript classes was covered in this guide. You can learn more on the Object Oriented Programming section of the MoonScript documentation.

不是所有的MoonScript的类功能都在这篇中会讲到。学习更多关于OOP的部分,可以看MoonScript的文档。

-->

Posted July 05, 2015 by leafo (@moonscript) · Tags: lua, moonscript Tweet

MoonScript’s class system is great balance of functionality and brevity. It’s simple to get started with, doesn’t impose many restrictions, and is incredibly flexible when you need to do advanced things or bend the rules.
MoonScript的类系统功能设计的简洁明了,容易上手,无过多的牵绊, 是一款可以方便灵活使用的高级货。
Even if you have no intention of using MoonScript, understanding the class system implementation is a good exercise for understanding some of the more complicated parts of Lua.
即使你对MoonScript不感冒,理解类系统的实现,也是一次对lua高端用法深入理解的契机。
  • 例子(A simple example)
      类对象(The class object) 基类(The base object)
  • 类与继承(Classes with inheritance)
  • 类使用的提示与技巧(Class tips and tricks)
      添加__tostring和其它的meta方法。(Adding __tostring and other metamethods) 添加类声明的new方法。(Adding a new method to a class after declaration) 转换一个已经存在表结构为一个实例。(Converting an existing table to an instance) 给一个实例添加__index的meta属性。(Adding __index metafield to an instance)
  • 特性增强(Future improvements)
  • 尾声(Closing)
  • 简单例子(A simple example)

    Lets start with a typical class in MoonScript:
    我们来创建一个典型的MoonScript类。
    None
    1
    2
    3
    4
    5
    
    class Player
      new: (@x, @y) =>
    
      say_hello: =>
        print "Greetings! I'm at #{@x}, #{@y}"
    And take a look at the generated Lua: (Warning: there’s a lot going on, scroll past for analysis of each component)
    看由MoonScript翻译生成的Lua代码(提示:内容比较多,注意看每个部分。)
    None
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    local Player
    do
      local _base_0 = {
        say_hello = function(self)
          return print("Greetings! I'm at " .. tostring(self.x) .. ", " .. tostring(self.y))
        end
      }
      _base_0.__index = _base_0
      local _class_0 = setmetatable({
        __init = function(self, x, y)
          self.x, self.y = x, y
        end,
        __base = _base_0,
        __name = "Player"
      }, {
        __index = _base_0,
        __call = function(cls, ...)
          local _self_0 = setmetatable({}, _base_0)
          cls.__init(_self_0, ...)
          return _self_0
        end
      })
      _base_0.__class = _class_0
      Player = _class_0
    end
    Lets go from the outside in. The result of the class expression is a new local variable called Player. Nothing else is made available on the calling scope.
    彻底看一下,类表达式产生了一个叫Palyer的局部变量, 超出作用域的调用不了。
    The class’s internal objects are created inside of a Lua do end block, this ensures that they are scoped to just the class in question. The two internal objects are _class_0 and _base_0.
    类的内部对象,是在do和end 声明块之间定义的,确保子对象的作用域只在类里,两个内部对象,_class_0和_base_0。
    The resulting local, Player is assigned _class_0.
    结果也是局部的,Player最终的是用_class_0赋予的。
    The numbers at the end of these variables are not fixed, they come from MoonScript’s local name generator. They will increment if you nest classes. You should never write code that depends on their names.
    这些变量结尾的数字是不能改的, 是由MoonScript的本地名称生成的。如果你有新类,他会自增长,你写的代码不要和这名字产生直接的依赖。

    类对象(The class object)

    The class object, aka _class_0 in the generated code, is a Lua table that represents the class. To create a new instance we call the class object as if it were a function. We can see here that it’s not actually a function.
    类对象, aka _class_0 是一个生成的代码,是lua的table结构来表示类,创建一个新的实例,我们调用调用类对象就像他是函数一样,但实际上它又不是一个函数。
    In order to make a Lua table callable it must implement the __call metamethod.
    为了让一个lua table可调用,就必须实现一个__call的meta方法。
    Here’s the extracted class object’s creation:
    这里展开了类对象的创建
    None
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    local _class_0 = setmetatable({
      __init = function(self, x, y)
        self.x, self.y = x, y
      end,
      __base = _base_0,
      __name = "Player"
    }, {
      __index = _base_0,
      __call = function(cls, ...)
        local _self_0 = setmetatable({}, _base_0)
        cls.__init(_self_0, ...)
        return _self_0
      end
    })
    The Lua function setmetatable sets the metatable of the first argument to the second argument. It then returns the first argument. This means the value of _class_0 is the modified version of the first table.
    lua的setmetatables函数设置,第一个参数到第二个参数的meta表结构(metatable)。返回的是第一个参数。意味_class_0的值第一个talbe的版本是可被编辑的。
    The table _class_0 is very basic. It has the constructor we created (with new) stored in __init, the base object stored in __base and the name of the class stored in __name.
    表 _class_0非常的基础。它是我们创建并存储在__init中的一个构造器, 基类对象是存在 __base中,并且类的名存在__name中。 Unlike the generated names, these names are unchanging and safe to use in your code. Because they are stored directly on the class object we can access them with dot syntax: 不像自动生成的那些名字,这个名字不能改变,可以安全的在你的代码中使用。因为它是直接存在类对象中的,我们可以直接用"."进行访问。
    print(Player.__name) --> prints "Player"

    Two metafields are provided on the class objects metatable: _index and _call.

    meta字段(metafiels)被提供在class对象的meta表中(metatable)。

    The __call function is what is called when we create a new instance: Player() It’s responsible for creating a new table to be the instance, providing it with a metatable, then calling the constructor.

    __call函数会在我们创建一个新实例时被调用:Player()的职责是把一个新表给成一个实例,提供一个meta表(metatable), 当我们调用构造函数时。

    You can can see how the base0 is used directly as the metatable of the object.

    你可看到base0是如何被用作对象的meta表的。(metatable)

    Additionally, the class object has an __index metafield set to the base. This has a lot of implications. The most important is you can access any fields from base directly on the class object, assuming they haven’t been shadowed by any fields directly on the class object.

    The base object

    local base0 = { sayhello = function(self) return print("Greetings! I'm at " .. tostring(self.x) .. ", " .. tostring(self.y)) end } _base0.index = base0 base0.class = class0 ```

    The base object, _base0 is a regular Lua table. It holds all the instance methods of the class. Our example from above implemented a say_hello method which is compiled directly into the base.

    The base object has a circular reference to itself in the __index field.

    This lets us use the base object directly as the metatable of instances. The __index property is where instance methods are fetched from. Since it points to itself, the instance methods can be pulled directly from the metatable without any indirection.

    Likewise, this also lets us implement other metamethods directly as instance methods of the class. I'll have an example below.

    It’s a very cool concept, and definitely worth taking a moment to understand.

    Lastly, a reference to the class placed on the base object with the name __class. This is how the @@ operator accesses the class object.

    Classes with inheritance

    Super invocation has changed a bit in MoonScript 0.4.0 Classes that inherit from other classes in MoonScript introduce a few more ideas. The extends keyword is used for inheritance:

    class SizedPlayer extends Player new: (@size, ...) => super ...

    say_hello: => super! print "I'm #{@size} tall"

    Here’s the resulting Lua:

    local SizedPlayer do local parent0 = Player local base0 = { sayhello = function(self) _parent0.sayhello(self) return print("I'm " .. tostring(self.size) .. " tall") end } _base0.index = base0 setmetatable(base0, parent0.base) local class0 = setmetatable({ init = function(self, size, ...) self.size = size return parent0.init(self, ...) end, base = base0, _name = "SizedPlayer", _parent = parent0 }, { _index = function(cls, name) local val = rawget(base0, name) if val == nil then return _parent0[name] else return val end end, _call = function(cls, ...) local _self0 = setmetatable({}, base0) cls.init(self0, ...) return self0 end }) base0.class = class0 if parent0.inherited then parent0._inherited(parent0, _class0) end SizedPlayer = class0 end

    The majority of the generated code is the same as a regular class. Here are the differences:

    local parent0 = Player There’s a new local variable inside the do end block called parent0 that holds a reference to the parent class.

    local base0 = { -- ... } base0.index = base0 setmetatable(base0, parent0.base)

    The metatable of the base is set to the base of the parent class. This establishes the inheritance chain for instances. If a method can’t be found on the class’s base, then the parent class’s base is automatically searched due to how __index works.

    There’s a slight disadvantage to this. Metamethods are fetched with rawget, so metamethod inheritance does not work by default. We can work around this with the __inherited callback discussed below.

    local class0 = setmetatable({ -- ... _parent = _parent0 }, { -- ... }

    The parent class is stored on the class object in a field called __parent. This gives you an easy way to reference the parent class object.

    { _index = function(cls, name) local val = rawget(base0, name) if val == nil then return _parent0[name] else return val end end, -- ... }

    The __index metafield on the class object is now a function, instead of a reference to the base (which is a table). rawget is used control the precedence of the properties. If the field can’t be found directly on the base then the parent class is searched.

    Remember that class objects also pull fields from their bases, so this has the effect of searching both the parent class object and the parent class’s base. Even though we've used rawget on the base, we can still get access to the parent class’s base.

    if parent0.inherited then parent0.inherited(parent0, class0) end

    Lastly, we now have a class callback. When a subclass is created and the parent class has a method __inherited then it is called with the class object that has just been created.

    The __inherited method works directly with class objects, no instances are involved.

    local base0 = { sayhello = function(self) _parent0.say_hello(self) return print("I'm " .. tostring(self.size) .. " tall") end }

    In the example I included a method that calls super. All MoonScript does is provide sugar for calling the method of the same name on the parent class.

    Class tips and tricks

    Now that you have an understanding of how a class in MoonScript is implemented, it’s easy to see how we can work with the internals to accomplish new things.

    Adding __tostring and other metamethods

    If you want your instances to have a string representation you can implement a __tostring method in the metatable.

    As we saw above, the metatable has an __index field set to itself, we just need to implement metamethods as instance methods:

    class Player new: (@x, @y) =>

    __tostring: => "Player(#{@x}, #{@y})"

    print Player(2, 8) --> "Player(2, 8)"

    All of Lua’s metamethods work (except __index, see below). Here’s an example of a vector class with overloaded operators:

    class Vector new: (@x, @y) =>

    __tostring: => "Vector(#{@x}, #{@y})"

    __add: (other) => Vector @x + other.x, @y + other.y

    __sub: (other) => Vector @x - other.x, @y - other.y

    __mul: (other) => if type(other) == "number" -- scale Vector @x * other, @y * other else -- dot product Vector @x * other.x + @y * other.y

    print Vector(1,2) * 5 + Vector(3,3) --> Vector(8, 13)

    I mentioned above that metamethod inheritance does not work:

    class Thing __tostring: => "Thing"

    class BetterThing extends Thing

    print BetterThing! --> table: 0x1057290 We can work around this by using the __inherited callback:

    class Thing tostring: => "Thing" __inherited: (cls) => cls.base.tostring = @tostring

    class BetterThing extends Thing

    print BetterThing! --> Thing

    Adding a new method to a class after declaration

    Now that we know about __base it’s easy to add new methods to classes that don’t have them.

    class Player new: (@name) =>

    -- add the new method Player.__base.jump = => print "#{@name} is jumping!"

    Player("Adam")\jump! --> Adam is jumping!

    We can extend this concept even further to dynamically generate methods:

    class Player new: (@name) =>

    for dir in *{"north", "west", "east", "south"} @_base["go#{dir}"]: => print "#{@name} is going #{dir}"

    Player("Lee")\go_east! --> Lee is going east

    Converting an existing table to an instance

    Sometimes you might already have a table that you'd like to convert to an instance of a class without having to copy it. Now that we know how the __init method works we can use setmetatable to accomplish a similar result:

    class Rect area: => @w * @h

    some_obj = { w: 15, h: 3 }

    -- apply the metatable setmetatable(someobj, Rect._base)

    print some_obj\area! --> 45

    This same method can be used to convert on object from type to another.

    Adding __index metafield to an instance

    MoonScript uses the _index metafield on class instances in order to allow instance properties to be looked up. If we just replace _inde with another implementation without any consideration we would break the instance. We'll have to chain our custom __index with the old one.

    Here’s how we might implement getter methods:

    class Thing getters: { age: => os.time! - @created_at }

    new: => @created_at = os.time!

    mt = getmetatable @
    old_index = mt.__index
    
    mt.__index = (name) =>
      if getter = old_index.getters[name]
        getter @
      else
        if type(old_index) == "function"
          old_index @, name
        else
          old_index[name]
    

    t = Thing! print t.age

    Its’s important that you don’t try to access self (without rawget) within the __index metamethod, otherwise you'll cause an infinite loop. Writing that massive implementation in the constructor isn’t ideal. Here’s a base class that automatically upgrades anyone who inherits with getter functionality:

    class HasGetters getters: {} inherited: (cls) => oldinit = cls.init cls.init = (...) => oldinit @, ...

      mt = getmetatable @
      old_index = mt.__index
    
      mt.__index = (name) =>
        if getter = old_index.getters[name]
          getter @
        else
          if type(old_index) == "function"
            old_index @, name
          else
            old_index[name]
    

    class BetterThing extends HasGetters getters: { age: => os.time! - @created_at }

    new: => @created_at = os.time!

    t = BetterThing! print t.age

    The clever part here is replacing the __init method on the base class with a custom one that automatically injects support for getters.

    特性增强(Future improvements)

    The class system is far from perfect. Here are some future improvements that I'd like to add:

    There’s no way to determine which order methods are added to a class. If you're going to be triggering side effects from method creation then your options are limited. The MoonScript class meta-properties use double underscore just like Lua. If Lua ever decides to use any of the same names then there will be conflicts. Closing

    Not all of the functionality of MoonScript classes was covered in this guide. You can learn more on the Object Oriented Programming section of the MoonScript documentation.

    不是所有的MoonScript的类功能都在这篇中会讲到。学习更多关于OOP的部分,可以看MoonScript的文档。


    糖果实验室

    Openresty中文编程网
    IKBC经典机械键盘
    机械键盘领券优惠购买

    Write a Comment