In this tutorial, you will learn how to make datatype APIs, and how you can use them to model data. If you haven't, you should first look at the API Intermediate tutorial. To understand some of the things discussed in this tutorial, it is helpful to know at least the basics of Lua programming. For the Programming in Lua manual, go here. Specifically, you should read up on metatables and the method syntax.
Let's first try to answer the question: What is a datatype? Simply, a datatype is a blueprint for creating objects of the same type that hold a specific kind of data. If you didn't understand much of that sentence, that's okay. I'll break it down:
- The datatype is the thing we use to create new data. For example, we could create a datatype for storing a bank account entry.
- Objects are created from datatypes. They hold unique information in encapsulated variables initialized by the datatype (e.g., We can create a new bank entry from the bank entry datatype and it will contain all the variables pertaining to that bank account (account name, money, card number, etc.)).
- You can be certain that any object of type bank_account will hold this information and behave a certain way, based on how the datatype initializes the data and modifies it.
Let's look at how the vector type is used. (Vectors are basically positions, but they can also express angles, rotation, velocity, etc. If you want, look it up on Wikipedia.)
local v = vector.new(5, 2.3, 8)
local w = vector.new(8, 3, 3)
print("v.x = " .. v.x) --> v.x = 5
print("v.y = " .. v.y) --> v.y = 2.3
print("v.z = " .. v.z) --> v.z = 8
print("v = " .. v .. " and w = " .. w) --> v = 5,2.3,8 and w = 8,3,3
print("v + w = " .. v + w) --> v + w = 13,5.3,11
print("v * 4 = " .. v * 4) --> v * 4 = 20,9.2,32
print("The cross product of v and w is " .. v:cross(w)) --> The cross product of v and w is -17.1,49,-3.4
There's some weird things going on here if you haven't coded much Lua, so let's break it down.
- Lines 1 and 2 create new vector objects from the vector type. These are actually represented as tables, so we can access their members as table indices.
- In lines 4 - 6, we print the members which were initialized in the vector.new call.
- Lines 8 - 10 are examples of operator overloading for tables. Usually, Lua would throw errors at us for using arithmetic operators on tables, but the vector type provides a metatable (more on this later) for describing behavior for these operators.
- Line 11 shows a normal method call on a vector object. We use the colon syntax to specify that v is the object to pass to the vector.cross function. This is syntactic sugar for vector.cross(v, w).
First, we'll create a table to hold our type's methods, and a metatable for object access to type methods and operator overloading.
local vector = { }
local mt = { __index = vector }
Before we make any methods, we should have a way of creating new objects to use those methods.
function new(x, y, z)
local self = {
x = x or 0,
y = y or 0,
z = z or 0
}
return setmetatable(self, mt)
end
What we are really doing here is creating the new object in a table called self and setting its metatable so that any calls to methods it doesn't know about are redirected to the vector type. This way, we don't have to copy the functions to each object, which would be a waste of memory. We just reference the functions of the vector type and pass the object to them. Setting the metatable also allows us to define behavior of operators on the object that gets returned.
Let's implement the add and mul methods.
function vector:add(v)
return new(self.x + v.x, self.y + v.y, self.z + v.z)
end
function vector:mul(s)
return new(self.x * s, self.y * s, self.z * s)
end
Remember that self is auto-assigned to the passed object by syntactic sugar.
Finally, we should add support for using the operators rather than explicitly calling vector.add and vector.mul. We can do this with the metatable we made earlier.
function mt.__add(l, r)
return l:add(r)
end
function mt.__mul(l, r)
if type(l) == "table" then
return l:mul(r)
end
return r:mul(l)
end
Note that since the multiplication function takes a vector and a scalar (number), we must do type checking to see which order the parameters must be passed to the vector.mul function.
And now so that we can see the objects as strings, let's create behavior for automatic conversion to a string.
function vector:tostring()
return self.x .. "," .. self.y .. "," .. self.z
end
function mt:__tostring()
return self:tostring()
end
Now here is the whole code so far.
local vector = { }
local mt = { __index = vector }
function new(x, y, z)
local self = {
x = x or 0,
y = y or 0,
z = z or 0
}
setmetatable(self, mt)
return self
end
function vector:add(v)
return new(self.x + v.x, self.y + v.y, self.z + v.z)
end
function vector:tostring()
return self.x .. "," .. self.y .. "," .. self.z
end
-- More methods...
function mt.__add(l, r)
return l:add(r)
end
function mt:__tostring()
return self:tostring()
end
-- More metamethods...
As an exercise, try implementing more methods and operator overloads by yourself using the current vector.add function as a template.
Let's backtrack. In this tutorial you learned:
- How to implement a datatype API.
- How objects are useful for storing data and how they relate to real-world objects.