Functions within tables (LUA-Question)

Place to get help with not working mods / modding interface.
Post Reply
User avatar
IngoKnieto
Long Handed Inserter
Long Handed Inserter
Posts: 97
Joined: Mon Oct 03, 2016 9:29 am
Contact:

Functions within tables (LUA-Question)

Post by IngoKnieto » Sat Jul 06, 2019 2:51 pm

Hi everybody,
this is probably more a LUA then a factorio question, but I hope I'm still in the right place here... I want to store a function in a table and access other values in the table from within that function.
See this example:

In the on_entity_build event I create a table with the entity and some other values and store it in a global table:

Code: Select all

local h = {
entity = event.created_entity,
value_a = 1,
value_b = 2
}
table.insert(global.mytable,h)
Then later I can access and use the other values for example like this :

Code: Select all

for _,h in pairs(global.mytable) do
local sum = h.value_a + h.value_b
end
So far no problem, but I would like to bring this one step further and store a sum function within the table, so that i can just do: local sum = h.sum().
How does this work?

I was assuming something like this, but apparently you can't access value_a and value_b like this:

Code: Select all

local h = {
entity = event.created_entity,
value_a = 1,
value_b = 2,
sum = function()
return value_a + value_b
end
}
table.insert(global.mytable,h)

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 3740
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Functions within tables (LUA-Question)

Post by eradicator » Sat Jul 06, 2019 3:09 pm

A function does not gain magical access to a table just because it it part of it. The rules for which "upvalues" a function has access to are independant on what pointer you use to access the function (yea, i'm not terribly good at explaining this...).

In your example h would be an upvalue of the function and could thus be accessed:

Code: Select all

function() return h.value_a + h.value_b end
The question is: Why do you need a per-table function for something as trivial as a sum instead of using one function for all tables?

Code: Select all

function sum(x) return x.a + x.b end --[[global function]]
local sum = sum(h)
Author of: Hand Crank Generator, Screenshot Hotkey 2.0
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
IngoKnieto
Long Handed Inserter
Long Handed Inserter
Posts: 97
Joined: Mon Oct 03, 2016 9:29 am
Contact:

Re: Functions within tables (LUA-Question)

Post by IngoKnieto » Sat Jul 06, 2019 3:38 pm

The sum function is just a simplified example, the actual function I want to build is more complicated. And of course I can store the function somewhere else, I just thought it would be neat to pack it into the table.

So if I understand your first code example correct, it should work like this?

Code: Select all

local h = {
entity = event.created_entity,
value_a = 1,
value_b = 2,
sum = function()
return h.value_a + h.value_b
end
}
table.insert(global.mytable,h)
This doesn't work, if I call the function the error says: attempt to index global 'h' (a nil value)

User avatar
tiriscef
Long Handed Inserter
Long Handed Inserter
Posts: 76
Joined: Thu Mar 28, 2019 8:45 pm
Contact:

Re: Functions within tables (LUA-Question)

Post by tiriscef » Sat Jul 06, 2019 4:00 pm

This link should help you, I think: Object-Oriented Programming in Lua

Basically to gain access to the other fields in a table, you need to give the table as an argument to the function. Like this:

Code: Select all

local h = {
entity = event.created_entity,
value_a = 1,
value_b = 2,
sum = function(self)
return self.value_a + self.value_b
end
}

--and call the function like
local i = h.sum(h)
And Lua has an optional C++ like syntax to make this code look more like actual object oriented programming:

Code: Select all

local h = {
entity = event.created_entity,
value_a = 1,
value_b = 2,
}

function h:sum() 
return self.value_a + self.value_b
end

--and call the function like
local i = h:sum()

User avatar
IngoKnieto
Long Handed Inserter
Long Handed Inserter
Posts: 97
Joined: Mon Oct 03, 2016 9:29 am
Contact:

Re: Functions within tables (LUA-Question)

Post by IngoKnieto » Sat Jul 06, 2019 4:40 pm

Thank you! Both your examples work.
And I like the second one a little better as it is closer to what I was expecting :)

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 3740
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Functions within tables (LUA-Question)

Post by eradicator » Sat Jul 06, 2019 4:44 pm

IngoKnieto wrote:
Sat Jul 06, 2019 3:38 pm
I just thought it would be neat to pack it into the table.
Well, yea, maybe. But you're not creating one function (from what i understand) you're creating an extra copy of the function for every table element (i.e. wasting memory). Also debugging code when there's a function stored in the savegame can be annoying and i don't recommend it. It's far easier to apply bugfixes to the function if it's not permanently stored in the savegame.
IngoKnieto wrote:
Sat Jul 06, 2019 3:38 pm
So if I understand your first code example correct, it should work like this?
Sorry, my mistake, i thought the locality of h would be evaluated sooner. So to get the function with "h" as upvalue into the table you have to either push it in later:

Code: Select all

local h = {
  entity = event.created_entity,
  value_a = 1,
  value_b = 2,
}
h.sum = function() return h.value_a + h.value_b end
table.insert(global.mytable,h)
Or declare h local before assigning the table to it:

Code: Select all

local h; h = {
  entity = event.created_entity,
  value_a = 1,
  value_b = 2,
  sum = function() return h.value_a + h.value_b end
}
table.insert(global.mytable,h)
Author of: Hand Crank Generator, Screenshot Hotkey 2.0
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 3740
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Functions within tables (LUA-Question)

Post by eradicator » Sat Jul 06, 2019 4:59 pm

If you want object oriented programming syntax you should use metatables instead:

Code: Select all

local methods = {
  sum = function(self)
    return self.value_a + self.value_b
    end
  }

scipt.on_load(function()
  for _,h in pairs(global.mytable) do 
    setmetatable(h,{__index=methods})
    end
  end)

Which gives you the same syntax as koun stated above

Code: Select all

local sum = h:sum()
This uses the same function for all table elements and allows you to easily change the function (fix bugs) as it automatically reloads the function when you reload a savegame.
Author of: Hand Crank Generator, Screenshot Hotkey 2.0
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

User avatar
IngoKnieto
Long Handed Inserter
Long Handed Inserter
Posts: 97
Joined: Mon Oct 03, 2016 9:29 am
Contact:

Re: Functions within tables (LUA-Question)

Post by IngoKnieto » Sat Jul 06, 2019 8:00 pm

eradicator wrote:
Sat Jul 06, 2019 4:44 pm
IngoKnieto wrote:
Sat Jul 06, 2019 3:38 pm
I just thought it would be neat to pack it into the table.
Well, yea, maybe. But you're not creating one function (from what i understand) you're creating an extra copy of the function for every table element (i.e. wasting memory). Also debugging code when there's a function stored in the savegame can be annoying and i don't recommend it. It's far easier to apply bugfixes to the function if it's not permanently stored in the savegame.
You're right, I didn't think about this. Not beeing able to update the function code when loading a savegame would have caused me some headaches later, so thanks for the heads up ;)


I am using metatables now, however your code didn't work, this is what I am doing now:

Code: Select all

local func_sum = function(self)
   return self.value_a + self.value_b
end

script.on_load(function()
   for _,h in pairs(global.mytable) do 
      local sum = setmetatable(h, {__index = func_sum}) -- this line is also included in the build_entity event
   end
end)
Calling the function then works like this:

Code: Select all

local s = h.sum
I am not completely sure what I am doing here, but it works :)


EDIT: Ok, I definitely don't know what I am doing here. Your code works, and mine has some unwanted side-effects.
I ended up using your code example. Thanks again...

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 3740
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Functions within tables (LUA-Question)

Post by eradicator » Sun Jul 07, 2019 7:40 am

IngoKnieto wrote:
Sat Jul 06, 2019 8:00 pm
EDIT: Ok, I definitely don't know what I am doing here. Your code works, and mine has some unwanted side-effects.
I ended up using your code example. Thanks again...
Metamethods are complicated and i highly recommend reading more about them if you plan to use them longterm (lua manual). In this case: There's a significant difference between assigning a function or a table to the __index metamethod. If you assign a function that function has to serve a specific purpose (namely getting an element from the table), so you can't just assign any random function. If you assign a table it's basically "get me h[sum]! if you can't find it in h then look in methods instead!".
Author of: Hand Crank Generator, Screenshot Hotkey 2.0
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

eduran
Filter Inserter
Filter Inserter
Posts: 344
Joined: Fri May 09, 2014 2:52 pm
Contact:

Re: Functions within tables (LUA-Question)

Post by eduran » Sun Jul 07, 2019 7:56 am

IngoKnieto wrote:
Sat Jul 06, 2019 8:00 pm
EDIT: Ok, I definitely don't know what I am doing here. Your code works, and mine has some unwanted side-effects.
I ended up using your code example. Thanks again...
Instead of going down this route, I suggest you turn 'sum' into a normal function with a table argument. Why? You don't have to manage metatables. It is (slightly) faster. It is easier to understand what is happening when reading code (try to read the code of something like helmod and you will see what I mean). You don't have to juggle : an . syntax. Quite a few disadvantages for the fake object-oriented approach with few upsides.

If you are doing it just for the fun of it, that is of course fine. But it will most likely result in worse code.

PS: Yes, I am throwing rocks from within a glasshouse. Using metatables and 'object-orienteded' Lua in LTNT was not a good idea.

User avatar
IngoKnieto
Long Handed Inserter
Long Handed Inserter
Posts: 97
Joined: Mon Oct 03, 2016 9:29 am
Contact:

Re: Functions within tables (LUA-Question)

Post by IngoKnieto » Sun Jul 07, 2019 11:56 am

Yeah, I'm beginning to understand that the object-oriented approach to LUA is maybe not the best idea. I am doing this just for the fun of it, but I think I'll put the bigger part of my functions outside of metatables.

Worth mentioning here is probably also this: https://lua-api.factorio.com/latest/Global.html If I understand this correct the metatables in the global tables are gone when you save the game, so without the "re-loading" in the on_load event this wouldn't work at all. And in multiplayer the on_load event is called for every player, that may also have some unwanted effects.


In case anyone is interested this is what I have done so far.
This is the code in the on_entity_build event, these are basically two different entities stored in two global tables together with a bunch of variables and the metatables:

Code: Select all

		
		-- create hangar object
		local h = {
			id = event.created_entity.unit_number,
			entity = event.created_entity,
			blocked_by = nil,
			spawn_position = spawn_pos,
			spawn_direction = spawn_dir
		}
		-- add table_functions
		setmetatable(h, {__index = table_functions})
		-- store hangar object in global table
		table.insert(global.hangars,h)

		
                (...)


		-- create runway object
		local r = {
			id = event.created_entity.unit_number,
			entity = event.created_entity,
			blocked_by = nil,
			spawn_position = spawn_pos,
			spawn_direction = spawn_dir,		
			runway_start = p_start,
			runway_end = p_end,
			runway_end_landing = p_end_landing
		}
		-- add table_functions
		setmetatable(r, {__index = table_functions})
		-- store runway object in global table
		table.insert(global.runways,r)


And this is the definition of the metatable functions and the on_load event.
The first function returns the ID of the ciruit network (if there is one connected to the entity). The second function compares the IDs of the two entity types by calling the first function.

Code: Select all

-- functions for metatables
local table_functions = {

-- returns id of the given circuit network
get_circuit_network_ID = function(self,wire_type)
	if self.entity.get_circuit_network(wire_type) then
		return self.entity.get_circuit_network(wire_type).network_id
	else
		return nil
	end
end,

-- checks if runways are connected by wire
has_connected_runways = function(self)
	if self:get_circuit_network_ID(defines.wire_type.red) then
		for _,r in pairs(global.runways) do
			if r:get_circuit_network_ID(defines.wire_type.red) == self:get_circuit_network_ID(defines.wire_type.red) then
				return true
			end
		end
	end
	if self:get_circuit_network_ID(defines.wire_type.green) then
		for _,r in pairs(global.runways) do
			if r:get_circuit_network_ID(defines.wire_type.green) == self:get_circuit_network_ID(defines.wire_type.green) then
				return true
			end
		end
		
	end
	return false
end
}


-- update metatable functions
script.on_load(function()
	for _,h in pairs(global.hangars) do 
		setmetatable(h, {__index = table_functions})
	end
	for _,r in pairs(global.runways) do
		setmetatable(r, {__index = table_functions})
	end
end)

And what I can do now to check if the entities are connected is just call it like this:

Code: Select all

for i,h in pairs(global.hangars) do
   -- check connected runways
   if h:has_connected_runways() == false then
      return
   end
end

There are probably 100 better solutions for this, but again this was done for fun and for my understanding of LUA, and regarding the last part it has paid of already ;)

Thanks for your help everybody!

eduran
Filter Inserter
Filter Inserter
Posts: 344
Joined: Fri May 09, 2014 2:52 pm
Contact:

Re: Functions within tables (LUA-Question)

Post by eduran » Sun Jul 07, 2019 4:11 pm

IngoKnieto wrote:
Sun Jul 07, 2019 11:56 am
Worth mentioning here is probably also this: https://lua-api.factorio.com/latest/Global.html If I understand this correct the metatables in the global tables are gone when you save the game, so without the "re-loading" in the on_load event this wouldn't work at all.
Not just metatables. Functions are not persisted, either.
IngoKnieto wrote:
Sun Jul 07, 2019 11:56 am
And in multiplayer the on_load event is called for every player, that may also have some unwanted effects.
If you handle it properly, it will also work in MP. But it is a potential cause for all kinds of issues (desyncs, players not able to join, straight up crashes).

User avatar
eradicator
Smart Inserter
Smart Inserter
Posts: 3740
Joined: Tue Jul 12, 2016 9:03 am
Contact:

Re: Functions within tables (LUA-Question)

Post by eradicator » Sun Jul 07, 2019 9:04 pm

eduran wrote:
Sun Jul 07, 2019 7:56 am
Instead of going down this route, I suggest you turn 'sum' into a normal function with a table argument. Why? You don't have to manage metatables. It is (slightly) faster. It is easier to understand what is happening when reading code (try to read the code of something like helmod and you will see what I mean). You don't have to juggle : an . syntax. Quite a few disadvantages for the fake object-oriented approach with few upsides.
I wholeheartedly agree. Infact i said in my very first answer that a normal function would be better (ok..i checked, and it's not worded very well :p). But @OP seemed to want an OOP approach.
IngoKnieto wrote:
Sun Jul 07, 2019 11:56 am
If I understand this correct the metatables in the global tables are gone when you save the game
They're gone when you *load* a game (not when you save it), because they're not stored in the savedata. The only thing that continutes to exist over a save/load cycle is normal data in the global table named "global", *everything else* will be gone. Btw this is also why i even bothered to include the on_load handler in the example - because it is absolutely essential to get metatables to work *at all*. on_load is pretty limited in what it can do btw, precisely to prevent most problems, basically you can only do changes that won't survive the next save/load cycle. https://lua-api.factorio.com/latest/Data-Lifecycle.html
Author of: Hand Crank Generator, Screenshot Hotkey 2.0
Mod support languages: 日本語, Deutsch, English
My code in the post above is dedicated to the public domain under CC0.

Post Reply

Return to “Modding help”

Who is online

Users browsing this forum: No registered users