Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.

Due to spam on this forum, all posts now need moderator approval.

 Entire forum ➜ MUSHclient ➜ Lua ➜ Metatable not working, and I can't fix it

Metatable not working, and I can't fix it

It is now over 60 days since the last post. This thread is closed.     Refresh page


Posted by Larkin   (278 posts)  Bio
Date Wed 21 Mar 2007 02:53 PM (UTC)
Message
I've been scratching my head all morning trying to figure out where I'm going wrong with this bit of code. Basically, I'm borrowing the idea from Ked to use GetInfo(232) as a method of expiring balances in a failsafe. (I'm sure my logic is confused in my scripts overall, but that's a separate issue.)

Here's what I'm using for my metatable, and I'm not seeing the expected values and notes when I set/get things. Any ideas?

lost_bals = {}

setmetatable(lost_bals,
{
	__index = 
	function (t, name)
		local b = rawget(t, name)

		Note("Table " .. tostring(t) .. ", key " .. name .. " has value " .. tostring(b))
		if (not b) then
			return false
		end

		if (b <= GetInfo(232)) then
			t[name] = nil
			return false
		end

		return true
	end,

	__newindex =
	function(t, name, val)
		if (val == nil) then
			Note("Table " .. tostring(t) .. ", key " .. name .. " deleted")
			rawset(t, name, nil)
			return
		end

		local v = (val or 5.0) + GetInfo(232)
		Note("Table " .. tostring(t) .. ", key " .. name .. " gets value " .. tostring(v))
		rawset(t, name, v)
	end
})
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #1 on Wed 21 Mar 2007 07:20 PM (UTC)
Message
These look reasonable a priori; how are you using it? And what are you expecting to see?

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,166 posts)  Bio   Forum Administrator
Date Reply #2 on Wed 21 Mar 2007 08:23 PM (UTC)
Message
I am guessing you are trying to store a "time when something expires", based on the current timer plus a number of seconds you supply? I hope that is it.

This line concerns me:


local v = (val or 5.0) + GetInfo(232)


You know val is not nil because you have a test just above. Thus it can just read:


local v = val + GetInfo(232)


The next problem is that _newindex is only called if the entry in the table does not exist. Thus, after you have set the time once, subsequent attempts to change it will simply store the value, as _newindex will not be called.

For example, in my test, initially it stored a large number, and then simply the supplied argument:


"nick"=215098.24469088
"nick"=22


Similarly, _index is only called if the entry does not exist, it isn't called every time. Thus, attempting to access a value (which already exists) will simply return the value. You need to keep the table empty for your idea to work. Then of course you have the issue of where to store the data. So, we make a second "proxy" table, which really stores the data, and the main table is the one with the metatable on it:


lost_bals = lost_bals or {}
lost_bals_proxy = lost_bals_proxy  or {}

setmetatable(lost_bals,
{
	__index = 
	function (t, name)
		local b = lost_bals_proxy [name]

		Note("Table " .. tostring(t) .. ", key " .. name .. " has value " .. tostring(b))
		if (not b) then
			return false
		end

		if (b <= GetInfo(232)) then
			lost_bals_proxy [name] = nil
			return false
		end

		return true
	end,

	__newindex =
	function(t, name, val)
		if (val == nil) then
			Note("Table " .. tostring(t) .. ", key " .. name .. " deleted")
			lost_bals_proxy [name] = nil
			return
		end

		local v = val + GetInfo(232)
		Note("Table " .. tostring(t) .. ", key " .. name .. " gets value " .. tostring(v))
		lost_bals_proxy  [name] = v
	end
})


Now if I do something like this:


lost_bals.nick = 11


And then this every second or so:


print (lost_bals.nick)


I will see this:


Table table: 00D79CE0, key nick has value 215995.74794162
true
Table table: 00D79CE0, key nick has value 215995.74794162
true

.... 11 seconds later:

Table table: 00D79CE0, key nick has value 215995.74794162
false
Table table: 00D79CE0, key nick has value nil
false


Notice that for 11 seconds it now returns the original value (timer + 11), and after 11 seconds it realises the time is up and returns false. Next time around the table item is not even there, so it says it has the value nil.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #3 on Wed 21 Mar 2007 08:32 PM (UTC)
Message
Oh yeah... *bonk self* I'd forgotten that the indexing metamethods only apply to keys that aren't in there yet. I've only ever used them proxy-style and haven't used them as was done here in so long that I'd forgotten it mattered. :-)

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,166 posts)  Bio   Forum Administrator
Date Reply #4 on Wed 21 Mar 2007 08:33 PM (UTC)
Message
By the way, putting "if" test conditions in brackets is a rather C way of doing things, it isn't necessary in Lua. So, the code could be neatened up a bit:


lost_bals = lost_bals or {}
lost_bals_proxy = lost_bals_proxy  or {}

setmetatable(lost_bals,
{
  __index = 
  function (t, name)
    local b = lost_bals_proxy [name]

    TraceOut ("Table key " .. name .. " has value " .. tostring(b))
    if not b then
      return false
    end

    if b <= GetInfo(232) then
      lost_bals_proxy [name] = nil
      return false
    end

    return true
  end,

  __newindex =
  function(t, name, val)
    if val == nil then
      TraceOut ("Table key " .. name .. " deleted")
      lost_bals_proxy [name] = nil
      return
    end

    local v = val + GetInfo(232)
    TraceOut ("Table key " .. name .. " gets value " .. tostring(v))
    lost_bals_proxy  [name] = v
  end
})



I have also converted Note to TraceOut in this version, because TraceOut only echoes its message if Trace is on - it is like an optional debug message. That is handy to use when you occassionally want to see your debug messages.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,166 posts)  Bio   Forum Administrator
Date Reply #5 on Wed 21 Mar 2007 09:33 PM (UTC)
Message
You could neaten things up a bit more by putting the proxy table inside the main table. This assumes you would never need the key _proxy, and uses rawget and rawset to make the proxy table if it doesn't exist yet. This method is a bit nicer because you could then add the metatable to lots of tables, without having to have separate proxy tables maintained for each one:


lost_bals = lost_bals or {}

function get_proxy_table (t)
  -- create proxy table if it doesn't exist yet
  if not rawget (t, "_proxy") then
    rawset (t, "_proxy", {})
  end -- if no proxy table yet
  
  -- now return the proxy table
  return t._proxy
end -- function get_proxy_table

setmetatable(lost_bals,
{
  __index = 
  function (t, name)

    local proxy = get_proxy_table (t)    
    local b = proxy [name]

    TraceOut ("Table key " .. name .. " has value " .. tostring(b))
    if not b then
      return false
    end

    if b <= GetInfo(232) then
      proxy [name] = nil
      return false
    end

    return true
  end,

  __newindex =
  function(t, name, val)
    local proxy = get_proxy_table (t)

    if val == nil then
      TraceOut ("Table key " .. name .. " deleted")
      proxy [name] = nil
      return
    end

    local v = val + GetInfo(232)
    TraceOut ("Table key " .. name .. " gets value " .. tostring(v))
    proxy  [name] = v
  end
})


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Larkin   (278 posts)  Bio
Date Reply #6 on Thu 22 Mar 2007 06:32 PM (UTC)

Amended on Thu 22 Mar 2007 06:35 PM (UTC) by Larkin

Message
Wow! So much great feedback! This is what I love about MUSHclient the most. :)

I've read lots of things about how to use metatables, and I don't recall one place that it was mentioned the __index and __newindex only worked for items that are not in the table already. In fact, I thought that was the whole point to having the rawget/rawset functions. Learn something new every day.

Thanks so much for the help!


Edit: I know that I don't need to enclose expressions in if statements with Lua. It's just a style thing for me, and sort of a habit from other languages. It's actually easier for me to read my code with them.
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #7 on Thu 22 Mar 2007 07:28 PM (UTC)
Message
PiL says it quite clearly, and the manual says it too but you have to look a bit:

Quote:

# "index": The indexing access table[key].

     function gettable_event (table, key)
       local h
       if type(table) == "table" then
         local v = rawget(table, key)
         if v ~= nil then return v end
         right here: we only look at __index if v is nil i.e. not in table
         h = metatable(table).__index
         if h == nil then return nil end
       else
         h = metatable(table).__index
         if h == nil then
           error(···);
         end
       end
       if type(h) == "function" then
         return h(table, key)      -- call the handler
       else return h[key]          -- or repeat operation on it
       end
     end

# "newindex": The indexing assignment table[key] = value.

     function settable_event (table, key, value)
       local h
       if type(table) == "table" then
         local v = rawget(table, key)
         if v ~= nil then rawset(table, key, value); return end
         right here: as above
         h = metatable(table).__newindex
         if h == nil then rawset(table, key, value); return end
       else
         h = metatable(table).__newindex
         if h == nil then
           error(···);
         end
       end
       if type(h) == "function" then
         return h(table, key,value)    -- call the handler
       else h[key] = value             -- or repeat operation on it
       end
     end


David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,166 posts)  Bio   Forum Administrator
Date Reply #8 on Thu 22 Mar 2007 07:34 PM (UTC)

Amended on Thu 22 Mar 2007 07:45 PM (UTC) by Nick Gammon

Message
You are right that the online reference for Lua is fairly silent about the way that __index and __newindex works (as David points out, you have to read the code). However the hard-copy book "Programming in Lua" - available from assorted places - is an excellent read in general, and describes metatables in some depth.

As the book mentions, when looking up a value in a table, if that key is not present, Lua normally returns nil, but first checks for the __index metamethod, and if present, calls that.

Similarly, before creating a new entry in a table, it checks for the __newindex metamethod.

Possible uses for this are:


  • __index : implement inheritence (that is, look up a value in a "parent" if not present in the child object), or implement default values (if a value is not present, return some default instead).

  • __newindex : redirect writes to a proxy table, or make a table read-only* (by raising an error when you attempt to add to it).


The proxy table idea was used in this post:

http://www.gammon.com.au/forum/?id=4904

Here I made a table with __index and __newindex to redirect attempts to access it, to actually reading/writing MUSHclient world variables.



* - footnote - using __newindex doesn't really make it read-only, because you can change existing entries. However it is useful for checking that you don't inadvertently "pollute" places (like the _G global environment) with variables that are probably best declared local.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Larkin   (278 posts)  Bio
Date Reply #9 on Thu 22 Mar 2007 07:35 PM (UTC)
Message
Okay. That's not what that code says to me. Lua is easier to read than most languages, but some of the tricks do make it a tad obscure sometimes.
Top

Posted by Nick Gammon   Australia  (23,166 posts)  Bio   Forum Administrator
Date Reply #10 on Thu 22 Mar 2007 07:41 PM (UTC)

Amended on Thu 22 Mar 2007 07:45 PM (UTC) by Nick Gammon

Message
Quote:

I thought that was the whole point to having the rawget/rawset functions.


Actually, the point is to avoid an infinite loop. Say you made some code that used __newindex to do something (eg. check that the data you are adding to a table is valid). Then if it passes the check, you want to really add it. Without rawset, the attempt to really add it would trigger the __newindex again, and you would have a loop. However rawset, bypasses the check and lets you directly add to the table, without triggering metamethods.

Similarly, you may want to use rawget to "really" get the data from a table, not whatever the __index metamethod returns.

In my code mentioned above for the MUSHclient variables, I didn't need to use rawset and rawget, because the data was really being stored somewhere else entirely (the MUSHclient world variable).

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,166 posts)  Bio   Forum Administrator
Date Reply #11 on Thu 22 Mar 2007 07:47 PM (UTC)
Message
Here is a short example of implementing defaults:


defaults = { hp = 100, mana = 50, movement = 33 }

t = { hp = 20 }

setmetatable (t, { __index = defaults } )

print (t.hp)    --> 20
print (t.mana)  --> 50


In my example the table t does not have an entry for mana, but the default is pulled from the __index table. This illustrates that you can use an actual table as the __index entry, as well as a function. Thus, defaults can be quickly implemented.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Larkin   (278 posts)  Bio
Date Reply #12 on Thu 22 Mar 2007 07:50 PM (UTC)
Message
I knew that rawget/rawset existed to avoid the infinite loop scenario. I just also thought that they were needed to retrieve the true value of existing keys, not realizing that these metamethods weren't called in that case. So, I had half the knowledge, and now I have the whole picture. My code's working great now. Thanks again!
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


42,653 views.

It is now over 60 days since the last post. This thread is closed.     Refresh page

Go to topic:           Search the forum


[Go to top] top

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.