implementing classes in Lua

  • while reading Lua, you may have stumbled upon something that looks like this:

    -- Declare a base `Entity` class...
    
    local Entity = Object:inherit()
    
    function Entity:tick() end
    function Entity:draw() end
    
    -- and an inheriting `Player` class.
    
    local Player = Entity:inherit()
    
    2025-02-08
    • this is the way people generally do object-oriented programming in the language.

      2025-02-08
    • for someone coming from a language like Java, where classes are a syntactic construct—class Cat extends Animal—it can feel weird to see them declared this way—as local variables, using regular functions to implement inheritance.

      2025-02-08
      • but worry not! this tutorial will hopefully clear up any confusion you might have, using beginner-friendly language, and simple examples.

        2025-02-08
  • metatables

    2025-02-08
    • before we start, we need to talk about metatables. these are Lua’s way of allowing users to overload operators.

      2025-02-08
      • operators include arithmetic: +, -, *, /, but also things like indexing tables a[b], creating new indices in tables a[b] = c, or function calls a(b, c, d).

        2025-02-08
      • we call it operator overloading, because we overload the default meaning of the operator with our own, custom definition.

        2025-02-08
    • we can set the metatable of a table using setmetatable(t, metatable).

      2025-02-08
      • the metatable is another table, that contains fields for overriding these operators.

        2025-02-08
        • the most important field of metatables we’ll be focusing on today is __index, which defines a fallback for the a[b] operator—and by extension, also a.b, which is syntactic sugar for a["b"].

          2025-02-08
    • __index

      2025-02-08
      • the __index field is used when an otherwise nil field is accessed in a table. consider this:

        local t = { a = 1 }
        print(t.b) --> nil
        
        2025-02-08
      • in this case, t does not have a metatable with __index, so nil is returned. to change this behaviour, we override __index by telling Lua a function to run whenever the key doesn’t exist.

        local fallback = { b = 2 }
        
        setmetatable(t, {
            -- The first argument is the table that's indexed, and the second argument is the index,
            -- i.e. the arguments map to `the_table[index]`.
            __index = function (the_table, index)
                return fallback[index]
            end,
        })
        
        print(t.b) --> 2
        
        2025-02-08
      • however, there is a more compact and faster way of doing this. __index is special, because in addition to being able to set it to a function, we can also set it to a table:

        setmetatable(t, {
            __index = fallback,
        })
        print(t.b) --> 2
        

        this avoids the need to allocate a local function, which can be costly if you run it many times in a game loop!

        2025-02-08
  • method call syntax

    2025-02-08
    • there’s one thing we need to get out of the way before we move on, and that is Lua’s method call syntax a:method(b). this syntax is equivalent to:

      a.method(a, b)
      
      2025-02-08
      • basically, the thing before : is passed as the first argument to the thing before :’s method function.

        2025-02-08
    • Lua also has a syntax sugar for declaring functions on tables:

      local t = {}
      
      function t.do_stuff()
          print("hi")
      end
      
      2025-02-08
    • to complement the : call syntax, there’s also the : function declaration syntax.

      function t:do_thing()
          self.aaa = 1
      end
      
      -- desugars to
      
      function t.do_thing(self)
          self.aaa = 1
      end
      

      as this example shows, this syntax simply inserts a parameter named self before all other parameters.

      2025-02-08
    • the call and declaration syntaxes are not tied together in any way, so the dot and colon syntax could be mixed however one wants, but it’s probably better not to.

      2025-02-08
      • bear in mind that your function declarations also serve the purpose of documentation, and using the : syntax in declarations makes it clearer you’re supposed to call the functions with the : syntax.

        2025-02-08
    • with that knowledge, we can move on to creating classes.

      2025-02-08
  • classes

    2025-02-08
    • we can use __index fallback tables to model classes quite easily.

      2025-02-08
    • let’s create a class Cat with two methods meow and feed:

      local Cat = {}
      
      function Cat:meow()
          print("meow")
      end
      
      function Cat:feed()
          self.food = self.food + 1
      end
      
      2025-02-08
    • we also need a method for creating cats, which I’ll call new:

      function Cat:new()
          local cat = {}
          cat.food = 10
          return cat
      end
      
      2025-02-08
    • we can now use the API like this:

      local kitty = Cat:new()
      Cat.meow(kitty)
      Cat.feed(kitty)
      print(kitty.food) --> 11
      

      but, note how we have to namespace the Cat functions specifically, and we cannot use the : method call operator yet. the table returned by Cat:new() does not have the methods meow and feed for that to work.

      2025-02-08
    • so to provide it with these methods, we can use our handy __index metamethod:

      function Cat:new()
          local cat = {}
          cat.food = 10
          -- setmetatable returns its first argument. How convenient!
          return setmetatable(cat, { __index = Cat })
      end
      
      2025-02-08
    • now we’ll be able to create cats that can meow on their own:

      kitty = Cat:new()
      kitty:meow()
      kitty:feed()
      print(kitty.food) --> 11
      
      2025-02-08
    • however, creating an extra metatable every single time we create a cat is pretty inefficient! we can exploit the fact that Lua doesn’t really care about metatable fields it doesn’t know about, and make Cat itself into a metatable:

      Cat.__index = Cat
      
      function Cat:new()
          local cat = {}
          cat.food = 10
          return setmetatable(cat, Cat)
      end
      
      2025-02-08
    • but note how we’ve declared Cat:new with the special method syntax. we call the method like Cat:new(), which desugars to Cat.new(Cat), which means that the implicit self parameter is already the Cat table! thus, we can simplify the call to setmetatable, to remove the redundant reference to Cat:

          return setmetatable(cat, self)
      
      2025-02-08
    • with all these improvements, here’s how the code looks so far.

      local Cat = {}
      Cat.__index = Cat
      
      function Cat:new()
          local cat = {}
          cat.food = 10
          return setmetatable(cat, self)
      end
      
      function Cat:meow()
          print("meow!")
      end
      
      function Cat:feed()
          self.food = self.food + 1
      end
      
      2025-02-08
  • inheritance

    2025-02-08
    • given this fairly simple way of creating classes, we can now expand this idea to inheritance.

      2025-02-08
    • conceptually, inheriting from a class is pretty simple: what we want to do, is to have all of the parent class’s methods available on the child class. I think you might see where this is going now: all we need to do to create a subclass, is to create a new class, whose metatable’s __index points to the parent class.

      2025-02-08
    • let’s rewrite our example with the kitty to generalise animals under a single class:

      Animal
      - food: integer
      : speak()
      : feed()
      
      Cat : Animal
      : speak()
      
      2025-02-08
    • so, starting with the base Animal class…

      local Animal = {}
      Animal.__index = Animal
      
      -- We don't create a `new` method, because we don't want people creating "generic" animals.
      -- This makes our class _abstract_.
      
      -- speak is a function that must be overridden by all subclasses, so we make it error by default.
      function Animal:speak() error("not implemented") end
      
      function Animal:feed()
          self.food = self.food + 1
      end
      
      2025-02-08
    • we can define a Cat class as a subclass of Animal:

      local Cat = {}
      -- We still need to override __index, so that the metatable we set in our own constructor
      -- has our overridden `speak()` method. 
      Cat.__index = Cat
      -- To be able to call `Animal` methods from `Cat`, we set it as its metatable.
      -- Remember that `Animal.__index == Animal`.
      setmetatable(Cat, Animal)
      
      function Cat:new()
          -- Ultra-shorthand way of initializing a class instance!
          -- No need to declare any temporary locals, we can pass the table into `setmetatable`
          -- right away.
          return setmetatable({
              food = 1,
          }, self)
      end
      
      -- Don't forget to override speak(), otherwise calling it will error out!
      function Cat:speak()
          print("meow")
      end
      
      2025-02-08
      • note now that declaring speak does not modify Animal. for that, we would need to set the __newindex metatable field on the Animal, not just __index.

        2025-02-08
    • now we can create instances of the Cat, and it will inherit the feed method from Animal:

      local kitty = Cat:new()
      kitty:speak()
      kitty:feed()
      print(kitty.food) --> 2
      
      2025-02-08
  • generalising

    2025-02-08
    • with all this, we are now ready to pack this subclassing functionality into a nicer package. speaking of packages, let’s create a module class.lua:

      local Class = {}
      Class.__index = Class
      
      return Class
      
      2025-02-08
    • now, let’s create a method for inheriting from the class.

      -- insert above `return Class`
      
      function Class:inherit()
          local Subclass = {}
          Subclass.__index = Subclass
          -- Note how `self` in this instance is the parent class, as we call the method like `SomeClass:inherit()`.
          setmetatable(Subclass, self)
          return Subclass
      end
      
      2025-02-08
    • this is going to let us cleanly inherit from classes, without needing to copy and paste all the __index and setmetatable boilerplate:

      local Class = require "class"
      local Sub = Class:inherit()
      
      2025-02-08
    • the other boilerplaty bit was initialisation, so let’s take care of that:

      -- insert below the `end` of `function Class:inherit()`
      
      -- By default, let's make the base `Class` impossible to instantiate.
      -- This should catch bugs if a subclass forgets to override `initialize`. 
      function Class:initialize()
          error("this class cannot be initialized")
      end
      
      -- `...` is Lua's notation for collecting a variable number of arguments
      function Class:new(...)
          local instance = {}
          -- `self` is the class we're instantiating, as this method is called like `MyClass:new()`
          setmetatable(instance, self)
          -- We pass the instance to the class's `initialize()` method, along with all the arguments
          -- we received in `new()`.
          self.initialize(instance, ...)
          return instance
      end
      
      2025-02-08
    • having that, we can now rewrite our Animal example to use our super-simple class library.

      local Class = require "class"
      
      ---
      
      local Animal = Class:inherit()
      
      -- We'll provide a convenience function for implementers, for initialising the food value,
      -- as well as any other base fields that may come up.
      function Animal:_initialize()
          self.food = 1
      end
      
      function Animal:speak()
          error("unimplemented")
      end
      
      function Animal:feed()
          self.food = self.food + 1
      end
      
      ---
      
      local Cat = Animal:inherit()
      
      -- Don't forget that our initialize() method errors by default, so it has to be overridden.
      function Cat:initialize()
          self:_initialize()
      end
      
      function Cat:speak()
          print("meow")
      end
      
      2025-02-08
    • having a nice class library like this makes things a lot more convenient. no longer do we have to mess with raw metatables! all we need to do is call inherit() or new(), and the magic is done for us.

      local kitty = Cat:new()
      kitty:speak()
      kitty:feed()
      print(kitty.food)
      
      2025-02-08
  • wrapping up

    2025-02-08
    • if you followed this tutorial from beginning to end, you now have a simple library for object-oriented programming in Lua, which supports creating classes, and inheriting from them.

      2025-02-08
    • to further your understanding, you may want to think about the following:

      2025-02-08
      • how would you call the superclass’s implementation of a method? can you think of ways to make it convenient and easy to remember?

        2025-02-08
      • our class library implements a Ruby-style Object:new(args) function for constructing new instances of our class. Python however, uses the syntax Object(args) for constructing instances of objects. can you think of a way to make our class library use the Python-style syntax?

        2025-02-08
      • define a 2D vector class using our class library. can you think of a way to make use of Lua’s native +, -, *, / math operators instead of named methods :add(), :sub(), :mul(), :div()?

        2025-02-08
      • try implementing an object:instanceof(Class) function, which checks that an object instance inherits from a given class.

        2025-02-08
      • Lua is a minimalistic, multi-paradigm language. can you think of any benefits and drawbacks towards doing object-oriented programming in Lua?

        2025-02-08
        • what are some problems for which this style of programming would lend itself as particularly good?

          2025-02-08
        • and similarly, what are some areas in which this style might not work so well?

          2025-02-08
  • further reading

    2025-02-08
    • you may wanna check these out for additional reference.

      2025-02-08
    • the Lua documentation on metatables—there’s lots of other operators you can overload!

      2025-02-08
    • rxi’s classic module—it’s an example of a good, but small class library that has all the features you’d ever need.

      2025-02-08