This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Yevano's profile picture

Api Advanced

Started by Yevano, 12 September 2013 - 05:24 PM
Yevano #1
Posted 12 September 2013 - 07:24 PM
This is a tutorial in the Ask a Pro Renewal Project.

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.
Still not sure what objects are? Think about it like this: We can model any real-world characteristics and behavior using objects. If we want to use our code to hold information about cars, we can have a Car type. It could then hold information about the car's model, mileage, top speed, etc. Those are all characteristics of cars. What about behavior? A car can turn, go faster, back up, and sound its horn. We model characteristics with members (variables) and behavior with methods (functions).

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).
We'll now make our own basic implementation of the vector API using the original as a guide. I'll do this in small increments of code, then display the final, uncommented code at the end.

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.
​Sorry it took so long to get this tutorial out. Just haven't had much time/am lazy. As always, feedback is appreciated. I'd especially like to know if anyone has a problem with how I explained objects and types.
Symmetryc #2
Posted 12 September 2013 - 07:49 PM
Correct me if I'm wrong, but that's not how you use __add; it passes the left and right values, not self and other.
MysticT #3
Posted 12 September 2013 - 08:05 PM
Correct me if I'm wrong, but that's not how you use __add; it passes the left and right values, not self and other.
"left and right" "self and other", different names, same thing.

function t.someFunc(self, other)
-- is the same as:
function t:someFunc(other)
-- just another way to define functions

EDIT: oh, I see what you mean. If the left value is not self, then yes, this won't work.
Edited on 12 September 2013 - 06:09 PM
Symmetryc #4
Posted 12 September 2013 - 08:09 PM
Correct me if I'm wrong, but that's not how you use __add; it passes the left and right values, not self and other.
"left and right" "self and other", different names, same thing.

function t.someFunc(self, other)
-- is the same as:
function t:someFunc(other)
-- just another way to define functions

EDIT: oh, I see what you mean. If the left value is not self, then yes, this won't work.
Yeah, so you won't be able to tell which one is self…
Yevano #5
Posted 12 September 2013 - 08:24 PM
Yeah, so you won't be able to tell which one is self…

self is always the first parameter, so it will always be the left-side value.
Symmetryc #6
Posted 12 September 2013 - 08:32 PM
Yeah, so you won't be able to tell which one is self…

self is always the first parameter, so it will always be the left-side value.
I don't think you understand what I mean by "left-side value".

local apple = function(left, right) -- this is not what I mean
end

local apple = left + right -- this is what I mean
– I'm talking about when someone adds something using your data type, the left thing that is added is passed as the first parameter, and the right thing is passed as the second parameter, so you don't inherently know which parameter is self.
Yevano #7
Posted 12 September 2013 - 08:40 PM
– I'm talking about when someone adds something using your data type, the left thing that is added is passed as the first parameter, and the right thing is passed as the second parameter, so you don't inherently know which parameter is self.

Ah, I was under the impression that the table had to be on the left side for the call to even be made in the first place. +1 for the correction; I'll change the code in the OP.

EDIT: This is more of an issue if one of the sides is something other than a vector, for example with a mul method where you have a vector and a scalar. The code will actually work in all cases, but I agree it makes more sense to explicitly label the metamethod parameters rather than using the object passing syntax.
Symmetryc #8
Posted 12 September 2013 - 09:15 PM
– I'm talking about when someone adds something using your data type, the left thing that is added is passed as the first parameter, and the right thing is passed as the second parameter, so you don't inherently know which parameter is self.
EDIT: This is more of an issue if one of the sides is something other than a vector, for example with a mul method where you have a vector and a scalar. The code will actually work in all cases, but I agree it makes more sense to explicitly label the metamethod parameters rather than using the object passing syntax.
Sorry if I'm going a little off topic, but I think it would be cool to discuss how it would be able to make this possible (it still pertains to the content of the tutorial, so I don't see how it could be too bad ;)/>). I think something like this would work, but I'm not sure.

setmetatableraw = setmetatable
setmetatable = function(t, mt)
  setmetatableraw(t, mt)
  mt.__addraw = mt.__add
  mt.__add = function(...)
    mt.__addraw(...)
    mt.__modadd(t, ...)
  end
end
Yevano #9
Posted 12 September 2013 - 09:29 PM

setmetatableraw = setmetatable
setmetatable = function(t, mt)
  setmetatableraw(t, mt)
  mt.__addraw = mt.__add
  mt.__add = function(...)
	mt.__addraw(...)
	mt.__modadd(t, ...)
  end
end

Interesting use of function detours. However, that only works if the __add field is set in the metatable before it is passed to the setmetatable function. Past that, I'm not actually sure what the code is supposed to be used for. Could you give a usage example?
Symmetryc #10
Posted 12 September 2013 - 10:04 PM

setmetatableraw = setmetatable
setmetatable = function(t, mt)
  setmetatableraw(t, mt)
  mt.__addraw = mt.__add
  mt.__add = function(...)
	mt.__addraw(...)
	mt.__modadd(t, ...)
  end
end

Interesting use of function detours. However, that only works if the __add field is set in the metatable before it is passed to the setmetatable function. Past that, I'm not actually sure what the code is supposed to be used for. Could you give a usage example?
Oops, didn't realize that, here:

setmetatableraw = setmetatable
setmetatable = function(t, mt)
  setmetatableraw(t, mt)
  mt.__addraw = mt.__add
  mt.__add = function(...)
    if mt.__addraw then
      mt.__addraw(...)
    end
    mt.__modadd(t, ...)
  end
end
This is how (in theory) it's supposed to work:

local apples = setmetatable({5}, {
  __modadd = function(self, left, right)
	return self==left and left[1] + right or left + right[1]
  end
})
print(apples + 5) --> 10
print(7 + apples) --> 12
Yevano #11
Posted 12 September 2013 - 10:22 PM
Interesting. Even better would be to just swap the values when passing them to the custom metamethod in such a way that the first param is always the object, no matter which side it lies on. This way the comparison with self is already done for you.
Symmetryc #12
Posted 12 September 2013 - 10:32 PM
Interesting. Even better would be to just swap the values when passing them to the custom metamethod in such a way that the first param is always the object, no matter which side it lies on. This way the comparison with self is already done for you.
But what if you wanted self and you wanted to know the order of the values in the equation? Maybe its parameters could go __modadd(self, other, bool), where bool is true when self is left? This way, if you didn't care about the order, you could just go __modadd(self, other) and it would be as the function that you described, but if you did you could still have that boolean available?
Yevano #13
Posted 12 September 2013 - 10:42 PM
But what if you wanted self and you wanted to know the order of the values in the equation? Maybe its parameters could go __modadd(self, other, bool), where bool is true when self is left? This way, if you didn't care about the order, you could just go __modadd(self, other) and it would be as the function that you described, but if you did you could still have that boolean available?

Exactly what I was thinking. Was too lazy to type that extra part out on my phone, though. :P/>
Symmetryc #14
Posted 12 September 2013 - 11:01 PM
But what if you wanted self and you wanted to know the order of the values in the equation? Maybe its parameters could go __modadd(self, other, bool), where bool is true when self is left? This way, if you didn't care about the order, you could just go __modadd(self, other) and it would be as the function that you described, but if you did you could still have that boolean available?

Exactly what I was thinking. Was too lazy to type that extra part out on my phone, though. :P/>
Great minds think alike, eh? :P/>
Lyqyd #15
Posted 13 September 2013 - 01:06 AM
This does need a side-agnostic version of an arithmetic operator, as the implementation in the tutorial is currently dependent on the vector coming first in the expression. I realize that that may well be how the vector API in ComputerCraft handles it, but if so, it is because dan's only usage of it is in context with other vectors or otherwise is used according to his intentions within the GPS API. An AaP tutorial should handle the other cases correctly. I'm also not enamored with declaring the functions after the table. Why not declare them inside the table? It's cleaner, and it shows more explicitly where the self variable used in them originates from. If the difference between api.function() and instance:function() hasn't been explained by this tutorial, it's certainly a good opportunity to dive into it.

This is definitely a good start, though!
Symmetryc #16
Posted 13 September 2013 - 09:01 AM
And while we're nit picking here, you might want to declare you functions like this, it's a bit cleaner :

local g = function(x)
end
Also, setmetatable returns the table that you are setting the metatable of :

-- old way
local a = function(B)/>/>/>
  local thing = setmetatable({}, B)/>/>/>
  return thing
end

-- new way
local a = function(B)/>/>/>
  return setmetatable({}, B)/>/>/>
end
This is also just a bit cleaner.

Forum screwed up my post, ignore those /> things.
theoriginalbit #17
Posted 13 September 2013 - 02:39 PM
And while we're nit picking here, you might want to declare you functions like this, it's a bit cleaner :

local g = function(x)
end
I don't think that, that is cleaner at all!

local function g(x)
  --# code
end
Is much cleaner IMO and it also doesn't have as much confusion for all the new programmers out there.

Forum screwed up my post, ignore those /> things.
Just put spaces either side of the b next time ( b ) doesn't get tampered with like (B)/> does
Engineer #18
Posted 13 September 2013 - 03:27 PM
And while we're nit picking here, you might want to declare you functions like this, it's a bit cleaner :

local g = function(x)
end
I don't think that, that is cleaner at all!

local function g(x)
  --# code
end
Is much cleaner IMO and it also doesn't have as much confusion for all the new programmers out there.

Forum screwed up my post, ignore those /> things.
Just put spaces either side of the b next time ( b ) doesn't get tampered with like ( B)/> does

I want to add something to the whole function syntaxis here.
if you do

local function x()

You are able to do this:

local function x()
   x() --# This is bad, but it when controlled it is ok
end

However, when you do this:

local x = function()
   x() --# attempt to call nil
end

To make that properly work you must do something like this:

local x
x = function() 
  x() --# again, bad practise. But when it is controlled recursion it is ok
end

In short, the locol function x() syntaxis always would work for your needs. And I agree it is less confusing for new people to Lua.
Yevano #19
Posted 13 September 2013 - 04:32 PM
This does need a side-agnostic version of an arithmetic operator, as the implementation in the tutorial is currently dependent on the vector coming first in the expression. I realize that that may well be how the vector API in ComputerCraft handles it, but if so, it is because dan's only usage of it is in context with other vectors or otherwise is used according to his intentions within the GPS API. An AaP tutorial should handle the other cases correctly. I'm also not enamored with declaring the functions after the table. Why not declare them inside the table? It's cleaner, and it shows more explicitly where the self variable used in them originates from. If the difference between api.function() and instance:function() hasn't been explained by this tutorial, it's certainly a good opportunity to dive into it.

This is definitely a good start, though!

I think it would be good to show an example of different-typed arguments with the multiplication operator where we take a vector and a scalar and return a new vector. Unless you feel an inline explanation is needed for the method syntax, I think it would be better to link to the PIL page pertinent to that syntax. Besides that, I actually did explain how the syntax was being used briefly in the tutorial. Finally, I've never liked the inline declaration of functions in tables for much more than small metatables. Apart from my own preference, I think it's important to demonstrate the table:func syntax. I think it's pretty. :P/>

I'll get to editing the OP a bit and edit this post once I'm done.

EDIT: Done.

Also, setmetatable returns the table that you are setting the metatable of :

Yep, added this.
Symmetryc #20
Posted 13 September 2013 - 09:15 PM
And while we're nit picking here, you might want to declare you functions like this, it's a bit cleaner :

local g = function(x)
end
I don't think that, that is cleaner at all!

local function g(x)
  --# code
end
Is much cleaner IMO and it also doesn't have as much confusion for all the new programmers out there.

Forum screwed up my post, ignore those /> things.
Just put spaces either side of the b next time ( b ) doesn't get tampered with like (B)/>/> does
And while we're nit picking here, you might want to declare you functions like this, it's a bit cleaner :

local g = function(x)
end
I don't think that, that is cleaner at all!

local function g(x)
  --# code
end
Is much cleaner IMO and it also doesn't have as much confusion for all the new programmers out there.

Forum screwed up my post, ignore those /> things.
Just put spaces either side of the b next time ( b ) doesn't get tampered with like ( B)/>/> does

I want to add something to the whole function syntaxis here.
if you do

local function x()

You are able to do this:

local function x()
   x() --# This is bad, but it when controlled it is ok
end

However, when you do this:

local x = function()
   x() --# attempt to call nil
end

To make that properly work you must do something like this:

local x
x = function() 
  x() --# again, bad practise. But when it is controlled recursion it is ok
end

In short, the locol function x() syntaxis always would work for your needs. And I agree it is less confusing for new people to Lua.
I guess it's just a matter of preference; I like that way for defining functions within tables, but I know that the other method has its benefits as well :)/>.
theoriginalbit #21
Posted 14 September 2013 - 12:11 AM
I guess it's just a matter of preference; I like that way for defining functions within tables, but I know that the other method has its benefits as well :)/>.
Indeed it is a matter of preference and as for defining functions within tables it is the only way to do it, it'll error if you try it the other way

Invalid

local t = {
  function x()
  end
}

Valid

local t = {
  x = function()
  end
}
Symmetryc #22
Posted 14 September 2013 - 09:56 AM
I guess it's just a matter of preference; I like that way for defining functions within tables, but I know that the other method has its benefits as well :)/>.
Indeed it is a matter of preference and as for defining functions within tables it is the only way to do it, it'll error if you try it the other way

Invalid

local t = {
  function x()
  end
}

Valid

local t = {
  x = function()
  end
}
Yeah, that's what I meant :P/>; I like defining my functions within the table, but some people define their functions outside of the tables like this:

local t = {}
t.something = function()
  print("hi")
end
So it really depends on what way you like doing things.