Eli Bell
Eli Bell

Reputation: 247

Lua : attempt to index a nil value; avoiding errors in conditionals

Let's say I have a giant table, something like:

test.test[1].testing.test.test_test

The table isn't guaranteed to exist. Neither are the tables containing it. I would like to just be able to do:

if test.test[1].testing.test.test_test then
   print("it exits!")
end

But of course, this would give me an "Attempt to index ? (a nil value)" error if any of the indices aren't yet defined. So many times, I'll end up doing something like this:

if test then
   if test.test then
      if test.test[1] then
         if test.test[1].testing then -- and so on

Is there a better, less-tedious way to accomplish this?

Upvotes: 5

Views: 2265

Answers (4)

lhf
lhf

Reputation: 72402

You can avoid raising errors by setting an __index metamethod for nil:

debug.setmetatable(nil, { __index=function () end })
print(test.test[1].testing.test.test_test)
test = {test = {{testing = {test = {test_test = 5}}}}}
print(test.test[1].testing.test.test_test)

You can also use an empty table:

debug.setmetatable(nil, { __index={} })

Upvotes: 6

Luatic
Luatic

Reputation: 11201

Without a helper function, the shortest variant is

if (((((test or {}).test or {})[1] or {}).testing or {}).test or {}).test_test then print"it exists!" end

simply replacing every t[k] or t.k with (t or {})[k] or (t or {}).k, which will lead to nils being replaced with {} such that subsequent indexing operations succeed (but may again yield nil).

Caveat: This will very likely create unnecessary garbage tables; it is unlikely that your Lua implementation will "optimize them out". It's also less convenient than writing yourself a helper. The helper I usually write uses a vararg to avoid having to create a temporary table:

function nilget(t, ...)
    for i = 1, select("#", ...) do
        if t == nil then return nil end
        t = t[select(i, ...)]
    end
    return t
end

Usage: nilget(test, "test", 1, "testing", "test", "test_test").

I would recommend not changing the debug metatable of nil; this effectively relaxes all indexing operations to nil-coalescing indexing operations, which is very likely to hide bugs.

Upvotes: 2

999PingGG
999PingGG

Reputation: 130

Another solution to consider is:

if
  test and
  test.test and
  test.test[1] and
  test.test[1].testing and
  test.test[1].testing.test and
  test.test[1].testing.test.test_test
then
   print("it exits!")
end

But of course, I'd try to refactor this as to not need so much nesting.

Upvotes: 0

Asumu Takikawa
Asumu Takikawa

Reputation: 8523

You can write a function that takes a list of keys to look up and does whatever action you want if it finds the entry. Here's an example:

function forindices(f, table, indices)
  local entry = table

  for _,idx in ipairs(indices) do
    if type(entry) == 'table' and entry[idx] then
      entry = entry[idx]
    else
      entry = nil
      break
    end
  end

  if entry then
    f()
  end
end

test = {test = {{testing = {test = {test_test = 5}}}}}

-- prints "it exists"
forindices(function () print("it exists") end,
           test,
           {"test", 1, "testing", "test", "test_test"})

-- doesn't print
forindices(function () print("it exists") end,
           test,
           {"test", 1, "nope", "test", "test_test"})

As an aside, the functional programming concept that solves this kind of problem is the Maybe monad. You could probably solve this with a Lua implementation of monads, though it wouldn't be very nice since there's no syntactic sugar for it.

Upvotes: 3

Related Questions