Metatables: Difference between revisions

From Legacy Roblox Wiki
Jump to navigationJump to search
>Merlin11188
Some format editing
Adding ScriptTutorial template
 
(43 intermediate revisions by 10 users not shown)
Line 1: Line 1:
{{ScriptTutorial|hard|scripting}}
{{EmphasisBox|The metatables for [[Strings]] and all ROBLOX types are locked; however, in normal Lua (not RBX.lua) you can set the metatables of these objects using the debug library.[http://www.lua.org/manual/5.1/manual.html#5.9] |green|dark=yes}}
{{EmphasisBox|The metatables for [[Strings]] and all ROBLOX types are locked; however, in normal Lua (not RBX.lua) you can set the metatables of these objects using the debug library.[http://www.lua.org/manual/5.1/manual.html#5.9] |green|dark=yes}}
===What is a Metatable?===
==What is a Metatable?==


----
Metatables allow tables to become more powerful than before. They are attached to data and contain values called metamethods. Metamethods are fired when a certain action is used with the datum that it is attached to.
 
Metatables allow tables to become more powerful than before. They are attached to data and contain values called Metamethods. Metamethods are fired when a certain action is used with the datum that it is attached to.
You may think that if you have code like this:
You may think that if you have code like this:


local list = {1, 2}
<syntaxhighlight lang="lua">
print(list[3])
local list = {1, 2}
print(list[3])
</syntaxhighlight>


The code will search through the list for the third index in list, realize it's
The code will search through the list for the third index in list, realize it's
Line 16: Line 17:
there isn't one.
there isn't one.


===getmetatable and setmetatable===
==Manipulating Metatables==
 
----
There are two main functions when dealing with metatables: getmetatable and
setmetatable. You use setmetatable to give a table a metatable, and you use
getmetatable to retrieve the metatable of an object. Here's an example:
 
local x = {}
local metaTable = {}      -- metaTables are tables, too!
setmetatable(x, metaTable) -- Give x a metatable called metaTable!
print(getmetatable(x))
 
Output:


table: [hexadecimal digits]
===setmetatable() and getmetatable()===
The two primary functions for giving and finding a table's metatable, are [[setmetatable]] and [[getmetatable]].


===Metatable Demonstration===
<syntaxhighlight lang="lua">
local x = {}
local metaTable = {}      -- metaTables are tables, too!
setmetatable(x, metaTable) -- Give x a metatable called metaTable!
print(getmetatable(x)) --> table: [hexadecimal memory address]
</syntaxhighlight>


local list = {1, 2}
The setmetatable function also returns the table that you're setting the metatable of, so these two scripts do the same thing:
print("In table List, key \"z\" is "..tostring(list.z))


Output:
<syntaxhighlight lang="lua">
local x = {}
setmetatable(x, {})
</syntaxhighlight>


In table List, key "z" is nil
<syntaxhighlight lang="lua">
 
local x = setmetatable({}, {})
Now, look at what happens with metatables:
</syntaxhighlight>
 
local list = {1, 2}      -- A normal table
local metatable = {       -- A metatable
    __index = function(t, key)
        rawset(t, key, 0) -- Set t[key] to 0
        return t[key]    -- return t[key] (which is now 0)
    end
}
setmetatable(list, metatable) -- Now list has the metatable metatable
print("In table List, key \"z\" is "..tostring(list.z))
 
Output:
 
In table List, key "z" is 0
 
What happened there? <code>list</code> is a normal table, with nothing unusual about it. <code>metatable</code> is also a table, but it has something special: the <code>__index</code> metamethod. The <code>__index</code> metamethod is fired when <code>t[key]</code> is nil, or in this case, <code>list.z</code> is nil. Now, nothing would happen because the two tables (<code>list</code> and <code>metatable</code>) aren't linked. That's what the third line does: sets <code>list</code>'s metatable to <code>metatable</code>, which means that when <code>list</code> is indexed (<code>__index</code>) at an index that's nil (<code>list.z</code>), the function associated with <code>__index</code> in Metatable is run. The function at <code>__index</code> in <code>metatable</code> uses <code>rawset</code> to make a new value in the Table (or <code>list</code>). Then, that value is returned. So, <code>list.z</code> is set to 0, and then <code>list.z</code> is returned (which is 0).


===Metamethods===
===Metamethods===
Line 64: Line 45:
calling a table, to adding a table, to even dividing tables as well. Here's a list
calling a table, to adding a table, to even dividing tables as well. Here's a list
of metamethods that can be used:
of metamethods that can be used:
{| class="wikitable"
! style="width: 20em" | Method !! Description
|-
| __index(<var>table</var>, <var>index</var>)
| Fires when table[index] is indexed, if table[index] is nil. Can also be set to a table, in which case that table will be indexed.
|-
| __newindex(<var>table</var>, <var>index</var>, <var>value</var>)
| Fires when table[index] tries to be set (table[index] = value), if table[index] is nil. Can also be set to a table, in which case that table will be indexed.
|-
| __call(<var>table</var>, <var>...</var>)
| Fires when the table is called like a function, <var>...</var> is the arguments that were passed.
|-
| __concat(<var>table</var>, <var>value</var>)
| Fires when the .. concatenation operator is used on the table.
|-
| __unm(<var>table</var>)
| Fires when the unary – operator is used on the table.
|-
| __add(<var>table</var>, <var>value</var>)
| The + addition operator.
|-
| __sub(<var>table</var>, <var>value</var>)
| The – subtraction operator.
|-
| __mul(<var>table</var>, <var>value</var>)
| The * mulitplication operator.
|-
| __div(<var>table</var>, <var>value</var>)
| The / division operator.
|-
| __mod(<var>table</var>, <var>value</var>)
| The % modulus operator.
|-
| __pow(<var>table</var>, <var>value</var>)
| The ^ exponentiation operator.
|-
| __tostring(<var>table</var>)
| Fired when [[Function_Dump/Core_Functions#tostring_.28e.29|tostring]] is called on the table.
|-
| __metatable
| if present, locks the metatable so getmetatable will return this instead of the metatable and setmetatable will error. Non-function value.
|-
| __eq(<var>table</var>, <var>value</var>)
| The == equal to operator˚
|-
| __lt(<var>table</var>, <var>value</var>)
| The < less than operator˚; '''NOTE:''' Using the >= greater than or equal to operator will invoke this metamethod and return the opposite of what this returns, as greater than or equal to is the same as not less than.
|-
| __le(<var>table</var>, <var>value</var>)
| The <= operator˚; '''NOTE:''' Using the > greater than operator will invoke this metamethod and return the opposite of what this returns, as greater than is the same as not less than or equal to.
|-
| __mode
| Used in [[Weak Tables|weak tables]], declaring whether the keys and/or values of a table are weak.
|-
| __gc(<var>table</var>)
| Fired when the table is garbage-collected.
|-
| __len(<var>table</var>)
| Fired when the # length operator is used on the Object. '''NOTE:''' Only userdatas actually respect the __len() metamethod in Lua 5.1
|}


*__index(Table, Index) — Fires when Table[Index] is nil.
˚ Requires two values with the ''same'' metatable; does not work with a table and another random value.
*__newindex(Table, Index, Value) — Fires when Table[Index] = Value when Table[Index] is nil.
*__call(Table, arg) — Allows the table to be used as a function, arg is the arguments passed, Table(arg).
*__concat(Table, Object) — The .. concatenation operator.
*__unm(Table) — The unary – operator.
*__add(Table, Object) — The + addition operator.
*__sub(Table, Object) — The – subtraction operator.
*__mul(Table, Object) — The * mulitplication operator.
*__div(Table, Object) — The / division operator.
*__mod(Table, Object) — The % modulus operator.
*__pow(Table, Object) — The ^ exponentiation operator.
*__tostring(Table) — Fired when tostring is called on the table.
*__metatable — if present, locks the metatable so getmetatable will return this instead of the metatable and setmetatable will error. Non-function value.
*__eq(Table, Table2) — The == equal to operator˚
*__lt(Table, Table2) — The < less than operator˚
*__le(Table, Table2) — The <= operator˚
*__mode — Used in [[Weak Tables]], declaring whether the keys and/or values of a table are weak.
*__gc(Object) — Fired when the Object is garbage-collected.
*__len(Object) — Fired when the # operator is used on the Object.


˚ Requires two tables; does not work with a table and a number, string, etc. The tables must have the '''same''' metatable.
==Using Metatables==
 
===Using Metatables===


There are many ways to use metatables, for example the __unm metamethod (to make a table negative):
There are many ways to use metatables, for example the __unm metamethod (to make a table negative):
<syntaxhighlight lang="lua">


local table1 = {10,11,12}
local metatable = {
 
__unm = function(t) -- __unm is for the unary - operator
local metatable = {
local negated = {}
    __unm = function(t) -- __unm is for the unary operator -
for key, value in pairs(t) do
        local negated = {} -- the table to return
negated[key] = -value -- negate all of the values in this table
        for key, value in pairs(t) do
end
            negated[key] = -value  
return negated -- return the table
        end
end
        return negated -- return the negative Table!
}
    end
}
setmetatable(table1, metatable)
print(table.concat(-table1, "; "))
 
Output:


-10; -11; -12
local table1 = setmetatable({10, 11, 12}, metatable)
print(table.concat(-table1, "; ")) --> -10; -11; -12
</syntaxhighlight>


Here's an interesting way to declare things using __index:
Here's an interesting way to declare things using __index:
 
<syntaxhighlight lang="lua">
local t = {}
local metatable = {
__index = {x = 1}
}
   
   
local metatable = {
local t = setmetatable({}, metatable)
    __index = {x = 1}
print(t.x) --> 1
}
</syntaxhighlight>
setmetatable(t, metatable)
print(t.x)


Output:
__index was fired when x was indexed in the table and not found. Lua then searched through the __index table for an index called x, and, finding one, returned that.
 
1
 
__index was fired when x was accessed from the table. __index then defined x as 1 instead of nil; therefore, 1 was returned.


Now you can easily do that with a simple function, but there's a lot more where
Now you can easily do that with a simple function, but there's a lot more where
that came from. Take this for example:
that came from. Take this for example:
 
<syntaxhighlight lang="lua">
local table = {10, 20, 30}
local t = {10, 20, 30}
print(table(5))
print(t(5))
</syntaxhighlight>


Now, obviously you can't call a table. That's just crazy, but (surprise, surprise!)
Now, obviously you can't call a table. That's just crazy, but (surprise, surprise!)
with metatables you can.
with metatables you can.


local table = {10, 20, 30}
<syntaxhighlight lang="lua">
local metatable = {
local metatable = {
__call = function(t, param)
    __call = function(table, param)
local sum = {}
        local sum = {}
for i, value in ipairs(t) do
        for i, value in ipairs(table) do
sum[i] = value + param -- Add the argument (5) to the value, then place it in the new table (t).
            sum[i] = v + param -- Add the argument (5) to the value, then place it in the new table (t).
end
        end
return unpack(sum) -- Return the individual table values
        return unpack(sum) -- Return the individual table values
end
    end
}
}
 
setmetatable(table, metatable)
print(table(5))
 
Output:


15 25 35
local t = setmetatable({10, 20, 30}, metatable)
print(t(5)) --> 15 25 35
</syntaxhighlight>


You can do a lot more as well, such as adding tables!
You can do a lot more as well, such as adding tables!
<syntaxhighlight lang="lua">
local table1 = {10, 11, 12}
local table2 = {13, 14, 15}


local table1 = {10, 11, 12}
for k, v in pairs(table1 + table2) do
local table2 = {13, 14, 15}
print(k, v)
 
end
for k, v in pairs(table1 + table2) do
</syntaxhighlight>
    print(k, v)
end


This will error saying that you're attempting to perform arithmetic on a table.  Let's try this with a metatable.
This will error saying that you're attempting to perform arithmetic on a table.  Let's try this with a metatable.
 
{{code and output|fit=code|code=
local table1 = {10, 11, 12}
local metatable = {
local table2 = {13, 14, 15}
__add = function(t1, t2)
 
local sum = {}
local metatable = {
for key, value in pairs(t1) do
    __add = function(table1, table2)
sum[key] = value
        local sum = {}
end
        for key, value in pairs(table1) do
            if table2[key] ~= nil then -- Does this key exist in that table?
for key, value in pairs(t2) do
                sum[key] = value + table2[key]
if sum[key] then
            else                      -- If not, add 0.
sum[key] = sum[key] + value
                sum[key] = value
else
            end
sum[key] = value
        end
end
       
end
        -- Add all the keys in table2 that aren't in table 1
return sum
        for key, value in pairs(table2) do
end
            if sum[key] == nil then
}
                sum[key] = value
            end
        end
        return sum
    end
}
   
   
setmetatable(table1, metatable)
local table1 = setmetatable({10, 11, 12}, metatable)
setmetatable(table2, metatable)
local table2 = setmetatable({13, 14, 15}, metatable)
for k, v in pairs(table1 + table2) do
    print(k, v)
end


Output:
for k, v in pairs(table1 + table2) do
 
print(k, v)
1 23
end
2 25
|output=
3 27
1 23
 
2 25
If the two tables have two different __add functions, then Lua will go to table1
3 27
first and if it doesn't have an __add function, then it'll go to the second one. That
}}
means that you really only have to set the metatable of Table1 or Table2, but it's
nicer and more readable to set the metatable of both.
 
Here is one last example breaking away from using separate variables when it isn't necessary.
 
local t = setmetatable({
    10, 20, 30
}, {
    __call = function(a, b)
        return table.concat(a, b..' ')..b
    end
})
print('Tables contains '..t(1)) --> Tables contains 101 201 301


===Use Cases===
==Use Cases==


Now, I am well aware that you can do all of these as a simple function yourself,
Now, I am well aware that you can do all of these as a simple function yourself,
Line 226: Line 214:
For this one we will be using the __index metamethod just to make it simple:
For this one we will be using the __index metamethod just to make it simple:


{{Example|<pre>
<syntaxhighlight lang="lua">
local Table = {}
 
local function mathProblem(num)
local function mathProblem(num)
for i = 1, 20 do
for i = 1, 20 do
Line 239: Line 225:
end
end


local Metatable = {
local metatable = {
__index = function (object, key)
__index = function (object, key)
local num = mathProblem(key)
local num = mathProblem(key)
Line 247: Line 233:
}
}


local setmetatable(Table, Metatable)
local t = setmetatable({}, metatable)


print(Table[1]) -- Will be slow because it's the first time using this number.
print(t[1]) -- Will be slow because it's the first time using this number, so it has to run the math function.
print(Table[2]) -- will be slow because it's the first time using this number.
print(t[2]) -- will be slow because it's the first time using this number.
print(Table[1]) -- will be fast because it's just grabbing the number from the table.
print(t[1]) -- will be fast because it's just grabbing the number from the table.
</pre>}}
</syntaxhighlight>


===See Also===
==Rawset, Rawget, Rawequal==


http://www.lua.org/manual/5.1/manual.html#2.8
When playing with metatables, you may run into some problems.  What happens if you need to use the __index metamethod to create new values in a table, but that table's metatable also has a __newindex metamethod in it?  You'll want to use the Lua built-in function [[Function_Dump/Core_Functions#rawset_.28table.2C_index.2C_value.29|rawset]] to set the value without invoking any metamethods. Take the following code as an example of what happens if you '''don't''' use these functions.
 
<syntaxhighlight lang="lua">
local t = setmetatable({}, {
__index = function(self, i)
self[i] = i * 10 -- just as an example
return self[i]
end,
__newindex = function(self, i, v)
--don't do anything because we don't want you to set values to the table the normal way
end
})
print(t[1]) -- Causes a C-Stack overflow
</syntaxhighlight>
 
Now why would that cause a stack overflow?  Stack overflows happen when you try to call a function from itself too many times, but what would cause that to happen?  In the __index function, we set self[i] to a value, so when it gets to the next line, self[i] should exist, so it won't call the __index metamethod, right?
 
The problem is that __newindex doesn't let us set the value.  Its presence stops values from being added to the table with the standard t[i] = v method. In order to get past this, you use the rawset function.
 
<syntaxhighlight lang="lua">
local t = setmetatable({}, {
__index = function(self, i)
rawset(self, i, i * 10)
return self[i]
end,
__newindex = function(self, i, v)
--don't do anything because we don't want you to set values to the table the normal way
end
})
print(t[1]) -- prints 10
</syntaxhighlight>


http://www.lua.org/pil
==See Also==
*[http://www.lua.org/manual/5.1/manual.html#2.8 Lua 5.1 Reference Manual: Metatables]
*[http://www.lua.org/pil/13.html Programming in Lua: Metatables and Metamethods]


[[Category:Scripting_Tutorials]]
[[Category:Scripting_Tutorials]]

Latest revision as of 14:27, 28 April 2023

This is a hard, scripting related tutorial.

The metatables for Strings and all ROBLOX types are locked; however, in normal Lua (not RBX.lua) you can set the metatables of these objects using the debug library.[1]

What is a Metatable?

Metatables allow tables to become more powerful than before. They are attached to data and contain values called metamethods. Metamethods are fired when a certain action is used with the datum that it is attached to. You may think that if you have code like this:

local list = {1, 2}
print(list[3])

The code will search through the list for the third index in list, realize it's not there, and return nil. That's totally wrong. What really happens is the code will search through the list for the third index, realize it's not there, and then try to see if there's a metatable attached to the table, returning nil if there isn't one.

Manipulating Metatables

setmetatable() and getmetatable()

The two primary functions for giving and finding a table's metatable, are setmetatable and getmetatable.

local x = {}
local metaTable = {}       -- metaTables are tables, too!
setmetatable(x, metaTable) -- Give x a metatable called metaTable!
print(getmetatable(x)) --> table: [hexadecimal memory address]

The setmetatable function also returns the table that you're setting the metatable of, so these two scripts do the same thing:

local x = {}
setmetatable(x, {})
local x = setmetatable({}, {})

Metamethods

Metamethods are the functions that are stored inside a metatable. They can go from calling a table, to adding a table, to even dividing tables as well. Here's a list of metamethods that can be used:

Method Description
__index(table, index) Fires when table[index] is indexed, if table[index] is nil. Can also be set to a table, in which case that table will be indexed.
__newindex(table, index, value) Fires when table[index] tries to be set (table[index] = value), if table[index] is nil. Can also be set to a table, in which case that table will be indexed.
__call(table, ...) Fires when the table is called like a function, ... is the arguments that were passed.
__concat(table, value) Fires when the .. concatenation operator is used on the table.
__unm(table) Fires when the unary – operator is used on the table.
__add(table, value) The + addition operator.
__sub(table, value) The – subtraction operator.
__mul(table, value) The * mulitplication operator.
__div(table, value) The / division operator.
__mod(table, value) The % modulus operator.
__pow(table, value) The ^ exponentiation operator.
__tostring(table) Fired when tostring is called on the table.
__metatable if present, locks the metatable so getmetatable will return this instead of the metatable and setmetatable will error. Non-function value.
__eq(table, value) The == equal to operator˚
__lt(table, value) The < less than operator˚; NOTE: Using the >= greater than or equal to operator will invoke this metamethod and return the opposite of what this returns, as greater than or equal to is the same as not less than.
__le(table, value) The <= operator˚; NOTE: Using the > greater than operator will invoke this metamethod and return the opposite of what this returns, as greater than is the same as not less than or equal to.
__mode Used in weak tables, declaring whether the keys and/or values of a table are weak.
__gc(table) Fired when the table is garbage-collected.
__len(table) Fired when the # length operator is used on the Object. NOTE: Only userdatas actually respect the __len() metamethod in Lua 5.1

˚ Requires two values with the same metatable; does not work with a table and another random value.

Using Metatables

There are many ways to use metatables, for example the __unm metamethod (to make a table negative):

local metatable = {
	__unm = function(t) -- __unm is for the unary - operator
		local negated = {}
		for key, value in pairs(t) do
			negated[key] = -value -- negate all of the values in this table
		end
		return negated -- return the table
	end
}

local table1 = setmetatable({10, 11, 12}, metatable)
print(table.concat(-table1, "; ")) --> -10; -11; -12

Here's an interesting way to declare things using __index:

local metatable = {
	__index = {x = 1}
}
 
local t = setmetatable({}, metatable)
print(t.x) --> 1

__index was fired when x was indexed in the table and not found. Lua then searched through the __index table for an index called x, and, finding one, returned that.

Now you can easily do that with a simple function, but there's a lot more where that came from. Take this for example:

local t = {10, 20, 30}
print(t(5))

Now, obviously you can't call a table. That's just crazy, but (surprise, surprise!) with metatables you can.

local metatable = {
	__call = function(t, param)
		local sum = {}
		for i, value in ipairs(t) do
			sum[i] = value + param -- Add the argument (5) to the value, then place it in the new table (t).
		end
		return unpack(sum) -- Return the individual table values
	end
}

local t = setmetatable({10, 20, 30}, metatable)
print(t(5)) --> 15 25 35

You can do a lot more as well, such as adding tables!

local table1 = {10, 11, 12}
local table2 = {13, 14, 15}

for k, v in pairs(table1 + table2) do
	print(k, v)
end

This will error saying that you're attempting to perform arithmetic on a table. Let's try this with a metatable.

local metatable = {
	__add = function(t1, t2)
		local sum = {}
		for key, value in pairs(t1) do
			sum[key] = value
		end
		
		for key, value in pairs(t2) do
			if sum[key] then
				sum[key] = sum[key] + value
			else
				sum[key] = value
			end
		end
		return sum
	end
}
 
local table1 = setmetatable({10, 11, 12}, metatable)
local table2 = setmetatable({13, 14, 15}, metatable)

for k, v in pairs(table1 + table2) do
	print(k, v)
end

1 23 2 25

3 27

Use Cases

Now, I am well aware that you can do all of these as a simple function yourself, but there's a lot more than what you think it can do. Let's try a simple program that will memorize a number when a possibly laggy math problem is put into it.

For this one we will be using the __index metamethod just to make it simple:

local function mathProblem(num)
	for i = 1, 20 do
		num = math.floor(num * 10 + 65)
	end
	for i = 1, 10 do
		num = num + i - 1
	end
	return num
end

local metatable = {
	__index = function (object, key)
		local num = mathProblem(key)
		object[key] = num
		return num
	end
}

local t = setmetatable({}, metatable)

print(t[1]) -- Will be slow because it's the first time using this number, so it has to run the math function.
print(t[2]) -- will be slow because it's the first time using this number.
print(t[1]) -- will be fast because it's just grabbing the number from the table.

Rawset, Rawget, Rawequal

When playing with metatables, you may run into some problems. What happens if you need to use the __index metamethod to create new values in a table, but that table's metatable also has a __newindex metamethod in it? You'll want to use the Lua built-in function rawset to set the value without invoking any metamethods. Take the following code as an example of what happens if you don't use these functions.

local t = setmetatable({}, {
	__index = function(self, i)
		self[i] = i * 10 -- just as an example
		return self[i]
	end,
	__newindex = function(self, i, v)
		--don't do anything because we don't want you to set values to the table the normal way
	end
})
print(t[1]) -- Causes a C-Stack overflow

Now why would that cause a stack overflow? Stack overflows happen when you try to call a function from itself too many times, but what would cause that to happen? In the __index function, we set self[i] to a value, so when it gets to the next line, self[i] should exist, so it won't call the __index metamethod, right?

The problem is that __newindex doesn't let us set the value. Its presence stops values from being added to the table with the standard t[i] = v method. In order to get past this, you use the rawset function.

local t = setmetatable({}, {
	__index = function(self, i)
		rawset(self, i, i * 10)
		return self[i]
	end,
	__newindex = function(self, i, v)
		--don't do anything because we don't want you to set values to the table the normal way
	end
})
print(t[1]) -- prints 10

See Also