(An in-depth look into the MoonScript class implementation)

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类。

    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代码(提示:内容比较多,注意看每个部分。)

    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:

    这里展开了类对象的创建

    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 _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.
    
    <h3>特性增强(Future improvements)</h3> 
    
    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
    
    <blockquote>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.</blockquote> 
    
    不是所有的MoonScript的类功能都在这篇中会讲到。学习更多关于OOP的部分,可以看MoonScript的文档。