3

Is it possible for a table, when referenced without a key, to return a particular value rather than a reference to itself?

Let's say I have the following table:

local person = {
    name  = "Kapulani",
    level = 100,
    age   = 30,
}

In Lua, I can quite easily refer to "person.name", "person.level", or "person.age" and get the values as expected. However, I have certain cases where I may want to just reference "person" and, instead of getting "table: " I'd like to return the value of "person.name" instead.

In other words, I'd like person.x (or person[x]) to return the appropriate entry from the table, but person without a key to return the value of person.name (or person["name"]). Is there a mechanism for this that I haven't been able to find?

I have had no success with metatables, since __index will only apply to cases where the key does not exist. If I put "person" into a separate table, I can come up with:

local true_person = {
    ... -- as above
}

local env_mt = {
    __index = function(t, k)
        if k == 'person' then
            return true_person
        end
    end
}

local env = setmetatable( {}, env_mt )

This lets me use __index to do some special handling, except there's no discernable way for me to tell, from __index(), whether I'm getting a request for env.person (where I'd want to return true_person.name) or env.person[key] (where I'd want to return true_person as a table, so that 'key' can be accessed appropriately).

Any thoughts? I can approach this differently, but hoping I can approach this along these lines.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Kapulani
  • 95
  • 5
  • 2
    You are fighting the syntax of Lua to create your own DSL. Does it really provide any advantage? An example would help. Anyway, you could try the __call metamethod or simply add a :default() method to all such tables, with a general or table-specific implementation. – Tom Blodget Jul 17 '14 at 23:59
  • I'm definitely weighing the pros and cons. I've been playing around with **__call** and it is a bit closer to what I was looking for, although I was hoping to avoid the need for **()**. The advantage I'm hoping for is that, by limiting the amount of 'translating' between the existing syntax and Lua syntax, when Lua throws an error, the output is relatively meaningful and recognizable to the user. So, converting **&** to **and** and **|** to **or** isn't that strange, but translating to **person** to **person()** in some places might be a bit odd, when **person.name** requires no translation. – Kapulani Jul 18 '14 at 03:37

2 Answers2

2

You can do it when the table is being used as a string by setting the __tostring metatable entry:

$ cat st.lua
local person = {
    name  = "Kapulani",
    level = 100,
    age   = 30,
}

print(person)
print(person.name)
print(person.age)

setmetatable(person, {__tostring = function(t) return t.name end})
print(person)

$ lua st.lua
lua st.lua
table: 0x1e8478e0
Kapulani
30
Kapulani
Etan Reisner
  • 77,877
  • 8
  • 106
  • 148
  • This is very helpful and straightforward. What if the default value were not a string, but a number? __tonumber does not seem to be a valid metamethod. Is there a workaround to use a numeric value? I.e., use age instead of name? – Kapulani Jul 17 '14 at 22:13
  • 2
    @Kapulani That could be done, but I'd say it's a strong contract that __tostring should return a string. – Tom Blodget Jul 17 '14 at 23:59
  • 1
    The metamethod name is for the operation (and function) that is being performed. You can certainly return a number from that metamethod. That might be, as @TomBlodget says, a bit odd but in most cases it won't hurt anything (normal string use cases will just auto-convert the number to a string immediately anyway). The only times that will hurt is if someone tries to use a string metamethod/etc. on the returned value directly. – Etan Reisner Jul 18 '14 at 02:46
  • Even if I hijack **__tostring**, I'd have to force a **tostring()** call in many places (e.g., if I want **person** to return **person.age** and I'm doing a **person > 21** comparison). – Kapulani Jul 18 '14 at 03:43
  • I think I'm going to abandon my approach, but I will flag this as the answer as it technically achieves what I had initially asked. Thank you for the helpful critiques. – Kapulani Jul 18 '14 at 06:09
1

I am not sure that what you are asking for is a good idea because it flies in the face of compositionality. Usually one would expect the following two programs to do the same thing but you want them to behave differently

print(person.name)

local p = person
print( p.name )

Its also not very clear how assignment would work. person.age = 10 should change the age but person = otherPerson should change the reference to the perrson, not the age.

If you don't care about compositionality and are onyl reading data, then a more direct way to solve the problem is to have a query function that receives the fields encoded in a string

query("person.age")   -- 17
query("person.name")  -- "hugomg"
query("person")       -- 17; query gets to default to whatever it wants.

To keep the syntax more lightweight you can omit the optional parenthesis

q"person.age"
q"person"

Or you can extend the __index metamethod on the global table, _G

setmetattable(_G, { __index = function(self, key) return query(key) end })

print ( person_age )  -- You will need to use "_" instead of "." for the
                      -- query to be a valid identifier.
hugomg
  • 68,213
  • 24
  • 160
  • 246