Lof
Lof

Reputation: 113

Re-initialize table without losing references

I'd like to re-initialize a table without losing references to it.

What I want to achieve is defining tables in files, and when a file is changed (with a text editor) the file is reloaded, changing the table. Of course this doesn't change the table but creates a new instance, old references will still point to the old table.

Any suggestions?

EDIT: I want to elaborate on what I want to achieve. An example with game characters and weapons. I want to modify the weapons.lua and so affect the characters.

-- weapons.lua
sword = { damage = 3 }

-- characters.lua
character = { weapon = sword }

Adding a level of indirection (putting "sword" inside "weapons") like suggested by JWT doesn't help, unless I split character into { weaponTable = weapons, weaponKey = "sword" } but I don't see this as an option.

Upvotes: 1

Views: 574

Answers (3)

nobody
nobody

Reputation: 4264

Anchor everything that needs to survive in the global environment. Nesting is fine, and this doesn't have to be your primary reference. (You can still local things, but make sure to initialize those local variables from the global environment and update the global if you change the local.)

To initialize the global values, say

foo = foo or value                  -- if foo is always true-ish
bar = (bar == nil) and value or bar -- if bar may be `false`

To initialize or update tables, you can

foo = foo or { }
foo.bar = foo.bar or 23
foo.baz = foo.baz or 42
-- and so on...

but that's kinda icky, so maybe say

function reinit( new, old ) -- (re)initialize one level, prefer old
  if old == nil then  return new  end
  if type( old ) ~= "table" then  return old  end
  for k, v in pairs( new ) do
    if old[k] == nil then  old[k] = v  end
  end
  return old
end
function reset( new, old ) -- (re)initialize one level, prefer new
  if old == nil then  return new  end
  if type( old ) ~= "table" then  return new  end
  for k, v in pairs( new ) do  old[k] = v  end
  return old
end

and then just

foo = reinit( { bar = 23, baz = 42 }, foo ) -- only setting if not defined
-- or
foo = reset( { bar = 23, baz = 42 }, foo ) -- always overriding given fields

or maybe make it even more fancy and say

function traverse( t, path )
  local here, last, lastk, created = t
  -- follow path of several keys starting from t, creating tables as needed
  for k in path:gmatch "[^.]+" do
    k = tonumber( k ) or k -- convert all-number keys to integer (for arrays)
    local next = here[k]
    if not next then
      next, created = { }, true
      here[k] = next
    else
      created = false
    end
    lastk, last, here = k, here, next
  end
  return here, last, lastk, created
end
function repopulate( path, value, update )
  update = update or reinit -- pass 'reset' as 'update' for the other behavior
                       -- or something entirely different if that's what you need
  local here, last, lastk, created = traverse( _G, path )
  if type( value ) == "table" then
    update( value, here )
  else
    if created then  last[lastk] = nil  end -- created one level too much
    update( { [lastk] = value }, last )
  end
end

and then (with arbitrary nesting)

-- No need to create 'state' first if it doesn't exist yet!
-- (If it exists, it will get updated, otherwise it's created)
repopulate( "state.player.weapon", { kind = "sword", damage = 11 } )
-- Do keep in mind that generally update order is relevant -- you may want to
-- give a newly created player a default inventory, but you may not want to
-- "refill" the player's inventory on every reload.  So generally `repopulate`
-- with the parent and all child nodes for from-scratch creation, then
-- `repopulate` the child nodes that need to be checked & potentially updated
-- as well.
-- (So here you'd probably repopulate `state.player` first and then 
-- `state.player.weapon` or other fields only if they should be updated anyway.)
-- e.g.:
repopulate( "state.player", {
  x = 0, y = 0, hp = 100, mp = 100, level = 0, -- and so on
  weapon = { kind = "sword", damage = 11 }, -- etc. etc.
} )
-- on reload always force a sword-kind weapon, leave rest (damage, ...) as-is
repopulate( "state.player.weapon", { kind = "sword" }, reset )
-- or alternatively: only if player has no weapon, give them a sword
repopulate( "state.player.weapon", { kind = "sword", damage = 3 } )

And you can go further, add metamethods to hide some of that shuffling, define different update policies, ... – you've seen some of the possibilities, now go and build your own version that fits your style and your code.

(While you're free to use the above code in any way, please note that it was written ad-hoc in the browser. I did some testing, fixed some glitches, and it seems to work now, but don't be surprised if there's still one or two bugs hiding in there. So play with this, change it, break it (and see how/why it breaks), adapt and extend it, ... – but unless you completely understand what it does and can fix any bugs, I strongly suggest you write your own version, or just stick to the basics. You probably don't need everything that this does, and you're likely to need other things that this doesn't do. As this is a central part of the reloading/live-coding infrastructure and everything has to be adapted to be reload-compatible, any mismatch between your tooling and what you actually need will result in a lot of pain everywhere in your code. So if you need something like this, put in a day or two to make it work the way you need it to, or you will regret it.)


(Free bonus warning: If you do OOP, you'll probably have to store and retrieve your classes instead of creating them every time, otherwise old objects from previous iterations will miss code updates and still run their old methods. I've forgotten about that more than just a couple of times and wasted several hours pondering "why isn't it fixed now?!?" after repeatedly re-loading code... So remember to anchor your metatables, anchor your classes!)

Upvotes: 2

Sygmei
Sygmei

Reputation: 465

I don't know if it's exactly what you needed (As an ID is necessary) but I hope it will fit your needs.

meta = {
  tables = {},
  __call = function(arg, t)
    for k, v in pairs(t) do
      arg[k] = v
    end
  end, 

  __bnot = function(arg) 
    return arg.__key
  end,

  __newindex = function(arg, key, val) 
    meta.tables[arg.__key][key] = val 
  end,

  __index = function(arg, key) 
    return meta.tables[arg.__key][key] 
  end
}

function RefTable(arg)
  local newtable = {}

  if arg ~= nil then
    newtable.__key = arg
    setmetatable(newtable, meta)

    if meta.tables[arg] == nil then
      meta.tables[arg] = {}
    end
  else
    error("RefTable can't have nil key")
  end

  return newtable
end

-- Using the RefTable
sword = RefTable("Sword")
sword({damage = 3})
sword.cooldown = 10
character = {sword = sword}

print("base", sword.damage, sword.cooldown)
print("embed", character.sword.damage, character.sword.cooldown)

sword = RefTable("Sword")
sword({damage = 8, cooldown = 50})

print("embed2", character.sword.damage, character.sword.cooldown)
print(sword.__key, sword.cooldown)
ref = RefTable("Sword")
ref.cooldown = 1000
print(sword.cooldown)

Upvotes: 1

JWT
JWT

Reputation: 407

You could nest the tables in another table.

Before:

local a = { 1, 2, 3 }
local b = { 7, 8, 9 }
print(a[2] + b[2])  -- #=> 10

After:

local lookup = {
  a = { 1, 2, 3 },
  b = { 7, 8, 9 }
}
print(lookup.a[2] + lookup.b[2])  -- #=> 10

Then you can fully replace (or just update) a table in the lookup table and any dependent statements will use that updated value:

lookup.a = { 100, 50, 0 }
print(lookup.a[2] + lookup.b[2])  -- #=> 58

Upvotes: 1

Related Questions