This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
darkroom's profile picture

[Lua] Need to test for nil

Started by darkroom, 05 April 2013 - 07:54 AM
darkroom #1
Posted 05 April 2013 - 09:54 AM
Ok so my error is on line 142 and is "attempt to index ? (a function value)" Basically I need to test if a function exists in a container or not but I don't know why it doesn't work.
Here is my code

termW, termH = term.getSize()
running = true
function newObject(type)
		local object = {type = type or "object"}
		function object:description()
				return "I am a "..object.type.." object."
		end
		return object
end
function newRect(x, y, w, h, backgroundColor, supertype)
		local rect = {
				x = x or 1,
				y = y or 1,
				w = w or termW,
				h = h or termH,
				backgroundColor = backgroundColor
		}
		super = newObject(supertype or "rect")
		setmetatable(rect,{__index = super})
		function rect:collidesWithPoint(x,y)
				return x >= self.x and x <= self.w and y >= self.y and y <= self.h
		end
		function rect:collidesWithRect(rect)
				local a = {
						left = self.x,
						right = self.x + self.w,
						top = self.y,
						bottom = self.y + self.h
				}
				local b = {
						left = rect.x,
						right = rect.x + rect.w,
						top = rect.y,
						bottom = rect.y + rect.h
				}
				local lMost, rMost, tMost, bMost
				if a.left < b.left then
						lMost = a
						rMost = b
				else
						lMost = b
						rMost = a
				end
				if a.top < b.top then
						tMost = a
						bMost = b
				else
						tMost = b
						bMost = a
				end
				local x = rMost.left - lMost.right
				local y = bMost.top - tMost.bottom
				return x <= 0 and y <= 0
		end
		function rect:draw()
				if self.backgroundColor then
						term.setBackgroundColor(self.backgroundColor)
						for x=self.x, self.w do
								for y=self.y, self.h do
										term.setCursorPos(x,y)
										term.write(" ")
								end
						end
				else
						error("This rect is non-drawable")
				end
		end
		return rect
end
function newText(x, y, string, textColor, supertype)
		local text = {
				x = x or 1,
				y = y or 1,
				string = string or "",
				textColor = textColor or colors.white
		}
		super = newObject(supertype or "text")
		setmetatable(text,{__index = super})
		function text:draw()
				term.setCursorPos(self.x,self.y)
				term.setTextColor(self.textColor)
				term.write(self.string)
		end
		return text
end
function newTextButton(x, y, text, backgroundColor, textColor, action, supertype)
		local textButton = {
				text = newText(x+1,y+1,text,textColor),
				rect = newRect(x,y,x+#text+2,y+2,backgroundColor),
				action = action or function() end
		}
		super = newObject(supertype or "textButton")
		setmetatable(textButton,{__index = super})
		function textButton:draw()
				self.rect:draw()
				self.text:draw()
		end
		function textButton:handleEvents(e)
				if e[1] == "mouse_click" then
						if self:collidesWithPoint(e[2],e[3],e[4]) then
								self:action()
						end
				end
		end
end
function newContainer()
		local container = { }
		super = newObject("container")
		setmetatable(container,{__index = super})
		function container:addObject(object)
				table.insert(self,object)
		end
		function container:removeObject(object)
				table.remove(self,object)
		end
		function container:drawObjects()
				for _,v in pairs(self) do
						if v.draw then
								v:draw()
						end
				end
		end
		function container:handleObjectEvents(e)
				for _,v in pairs(self) do
						if v.handleEvents then
								v:handleEvents(e)
						end
				end
		end
		return container
end
function toggleBackgroundColor(self)
		if self.backgroundColor == colors.red then
				self.backgroundColor = colors.lime
		else
				self.backgroundColor = colors.red
		end
end
function quit()
		running = false
end
term.clear()
term.setCursorPos(1,1)
local container = newContainer()
container:addObject(newTextButton(10,10,"Quit",colors.red,colors.white,quit))
container:addObject(newTextButton(3,3,"Toggle",colors.red,colors.white,toggleBackgroundColor))
while running do
		container:drawObjects()	  
		container:handleObjectEvents({os.pullEvent()})
end
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1,1)
142 is the 3rd line of this function

function container:handleObjectEvents(e)
						    for _,v in pairs(self) do
										    if v.handleEvents then
														    v:handleEvents(e)
										    end
						    end
		    end
LBPHacker #2
Posted 05 April 2013 - 09:59 AM
Gonna be ninja'd anyways…

You can check if a function is really a function using this:
if type(yourFunction) == "function" then

EDIT: Misunderstood, sorry…
Telokis #3
Posted 05 April 2013 - 10:00 AM
Gonna be ninja'd anyways…

Not today… :s
darkroom #4
Posted 05 April 2013 - 10:01 AM
I need to test if the function exists :)/>
LBPHacker #5
Posted 05 April 2013 - 10:03 AM
I need to test if the function exists :)/>

Heh… Then I was right :D/> (cuz if a functions doesn't exist - if it's nil, type(func) == "function" will evaluate false)
darkroom #6
Posted 05 April 2013 - 10:04 AM
I will try that but the error is that i am indexing nil. That is the problem
MysticT #7
Posted 05 April 2013 - 10:05 AM
The problem is that the "container" table has the functions stored inside it. So, when you iterate the table with pairs, it gets those functions and then try to index them (in v.draw).
A solution would be to add the values to another table inside the "container" table, like this:

function newContainer()
  local container = {}
  super = newObject("container")
  setmetatable(container, { __index = super })
  container.objects = {}
  function container:addObject(object)
	table.insert(self.objects, object)
  end
  function container:removeObject(object)
	table.remove(self.object, object) -- NOTE: this won't look fot the object and remove it, you'll have to make a loop to iterate trhough the table and remove it yourself
  end
  function container:drawObjects()
	for _,v in pairs(self.objects) do
	  if v.draw then
		v.draw()
	  end
	end
  end
  function container:handleObjectEvents(e)
	for _,v in pairs(self.objects) do
	  if v.handleEvents then
		v:handleEvents(e)
	  end
	end
  end
  return container
end
darkroom #8
Posted 05 April 2013 - 10:07 AM
Thanks man first time working with containers in lua so its a process :P/>
Kingdaro #9
Posted 05 April 2013 - 10:08 AM
Or just use a numeric loop.


  function container:drawObjects()
    for i=1, #self do
      if self[i].draw then
        self[i]:draw()
      end
    end
  end
  function container:handleObjectEvents(e)
    for i=1, #self do
      if self[i].handleEvents then
        self[i]:handleEvents(e)
      end
    end
  end

It's way faster than using pairs anyway. I also recommend setting functions like "draw" and "handleEvents" as default in the objects for your container, so you don't have to check if the methods exist before calling them.
MysticT #10
Posted 05 April 2013 - 10:13 AM
No problem.
One more thing. You could make tables that contains the functions you want, and then use metatables to "add" them to your "objects", so you don't create new functions for every new object.
Example:

local myClass = {}

function myClass:doSomething(arg1, arg2)
  print("Hello world!")
  self.someVariable = arg1 + arg2
end

function newObject()
  local obj = {}
  obj.someVariable = 0
  setmetatable(obj, { __index = myClass })
  return obj
end

local testObject = newObject()
testObject:doSomething()

Or just use a numeric loop.


  function container:drawObjects()
	for i=1, #self do
	  if self[i].draw then
		self[i]:draw()
	  end
	end
  end
  function container:handleObjectEvents(e)
	for i=1, #self do
	  if self[i].handleEvents then
		self[i]:handleEvents(e)
	  end
	end
  end

It's way faster than using pairs anyway. I also recommend setting functions like "draw" and "handleEvents" as default in the objects for your container, so you don't have to check if the methods exist before calling them.
Using another table would be better (imho), since you know that the "objects" you are looking for will be there and nothing else.
It's true that using a numerical iterator is faster than pairs, so it probably would be better to use it instead (but also using another table).
And it's always good to check for values before using them, it prevents weird unexpected errors :P/>
darkroom #11
Posted 05 April 2013 - 10:23 AM
It still isn't working for some reason I am not adding the objects the the container here is the new code:

termW, termH = term.getSize()
running = true
function newObject(type)
	    local object = {type = type or "object"}
	    function object:description()
			    return "I am a "..object.type.." object."
	    end
	    return object
end
function newRect(x, y, w, h, backgroundColor, supertype)
	    local rect = {
			    x = x or 1,
			    y = y or 1,
			    w = w or termW,
			    h = h or termH,
			    backgroundColor = backgroundColor
	    }
	    super = newObject(supertype or "rect")
	    setmetatable(rect,{__index = super})
	    function rect:collidesWithPoint(x,y)
			    return x >= self.x and x <= self.w and y >= self.y and y <= self.h
	    end
	    function rect:collidesWithRect(rect)
			    local a = {
					    left = self.x,
					    right = self.x + self.w,
					    top = self.y,
					    bottom = self.y + self.h
			    }
			    local b = {
					    left = rect.x,
					    right = rect.x + rect.w,
					    top = rect.y,
					    bottom = rect.y + rect.h
			    }
			    local lMost, rMost, tMost, bMost
			    if a.left < b.left then
					    lMost = a
					    rMost = b
			    else
					    lMost = b
					    rMost = a
			    end
			    if a.top < b.top then
					    tMost = a
					    bMost = b
			    else
					    tMost = b
					    bMost = a
			    end
			    local x = rMost.left - lMost.right
			    local y = bMost.top - tMost.bottom
			    return x <= 0 and y <= 0
	    end
	    function rect:draw()
			    if self.backgroundColor then
					    term.setBackgroundColor(self.backgroundColor)
					    for x=self.x, self.w do
							    for y=self.y, self.h do
									    term.setCursorPos(x,y)
									    term.write(" ")
							    end
					    end
			    else
					    error("This rect is non-drawable")
			    end
	    end
	    return rect
end
function newText(x, y, string, textColor, supertype)
	    local text = {
			    x = x or 1,
			    y = y or 1,
			    string = string or "",
			    textColor = textColor or colors.white
	    }
	    super = newObject(supertype or "text")
	    setmetatable(text,{__index = super})
	    function text:draw()
			    term.setCursorPos(self.x,self.y)
			    term.setTextColor(self.textColor)
			    term.write(self.string)
	    end
	    return text
end
function newTextButton(x, y, text, backgroundColor, textColor, action, supertype)
	    local textButton = {
			    text = newText(x+1,y+1,text,textColor),
			    rect = newRect(x,y,x+#text+2,y+2,backgroundColor),
			    action = action or function() end
	    }
	    super = newObject(supertype or "textButton")
	    setmetatable(textButton,{__index = super})
	    function textButton:draw()
			    self.rect:draw()
			    self.text:draw()
	    end
	    function textButton:handleEvents(e)
			    if e[1] == "mouse_click" then
					    if self:collidesWithPoint(e[2],e[3],e[4]) then
							    self:action()
					    end
			    end
	    end
end
function newContainer()
	    local container = {objects = { }}
	    super = newObject("container")
	    setmetatable(container,{__index = super})
	    function container:addObject(object)
			    table.insert(self.objects,object)
	    end
	    function container:removeObject(object)
			    for k,v in pairs(self.objects) do
					    if v == object then
							    table.remove(self.objects,k)
					    end
			    end
	    end
	    function container:drawObjects()
			    print(#self.objects)
			    for _,v in pairs(self.objects) do
					    if v.draw then
							    v:draw()
					    end
			    end
	    end
	    function container:handleObjectEvents(e)
			    for _,v in pairs(self.objects) do
					    if v.handleEvents then
							    v:handleEvents(e)
					    end
			    end
	    end
	    return container
end
function toggleBackgroundColor(self)
	    if self.backgroundColor == colors.red then
			    self.backgroundColor = colors.lime
	    else
			    self.backgroundColor = colors.red
	    end
end
function quit()
	    running = false
end
term.clear()
term.setCursorPos(1,1)
local container = newContainer()
container:addObject(newTextButton(10,10,"Quit",colors.red,colors.white,quit))
container:addObject(newTextButton(3,3,"Toggle",colors.red,colors.white,toggleBackgroundColor))

while running do
	    container:drawObjects()
	    container:handleObjectEvents({os.pullEvent()})
end
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
term.setCursorPos(1,1)

MysticT #12
Posted 05 April 2013 - 10:32 AM
I don't see anything wrong. Are you getting an error or it's just not working as expected?

Btw, I have a similar gui api in my os. The file's on github if you are interested.
Kingdaro #13
Posted 05 April 2013 - 10:38 AM
Using another table would be better (imho), since you know that the "objects" you are looking for will be there and nothing else.
It's true that using a numerical iterator is faster than pairs, so it probably would be better to use it instead (but also using another table).
And it's always good to check for values before using them, it prevents weird unexpected errors :P/>/>/>
Using another table is safer, yes, but if you know what you're doing, combining both numeric indexes and string indexes is very efficient and way more convenient than a separate table.

I like to think of it as a bank, where the numbered indices are the money, and the methods are the people (how you use/get the money.) If you use pairs, you're essentially going to eventually try handing a person to a customer as a deposit. :lol:/>/> But the people and the money can live together in harmony as long as you search for just the money, using a numeric loop.

On the subject of checking values, it's not really needed when the values are there and empty/callable by default. I always just use a table full of empty methods for a base.


local myObject = {
  doSomething = function() end;
  doSomethingElse = function() end;
  draw = function() end;
  etc = function() end;
}

But yeah, safer coding practices are safer. I just like playing with fire. :P/>/>
darkroom #14
Posted 05 April 2013 - 10:42 AM
If you test the code you will see that for some reason the objects don't add to the container thats why I have the print(#self.objects) in the draw function to see if they are in the container. It always comes out to 0. Also I like your API thing is I am going for OOP like interhance so I can build of classes and make more and more complex things
remiX #15
Posted 06 April 2013 - 12:53 AM
Using a pairs loop is the problem. Just tested it now and it's giving me the same error with another program.

I made a re-write of your program with better function use and functionality
pastebin
I commented most of it, ask if you still need help
darkroom #16
Posted 06 April 2013 - 09:05 AM
The code looks good but it gets rid of the higherarchy and the way I would like to code can you please find out why my code isn't working.
remiX #17
Posted 06 April 2013 - 09:23 AM
Okay, what is the current code you have now?

Could you please pastebin it and indent it properly :P/>
Kingdaro #18
Posted 06 April 2013 - 09:38 AM
Also holy god what is up with the super hardcore tabbing!?

I meant to ask about it when making my first post and I now just remembered to do so.
darkroom #19
Posted 06 April 2013 - 09:51 AM
hummmm sorry guys here is the pastebin: http://pastebin.com/VjfZXuSD
Smiley43210 #20
Posted 06 April 2013 - 04:37 PM
After some scrounging around, I found something. Look really, really, closely at your newTextButton function.

Found anything? Rather, didn't find something?

Solution, tested, drawing works:
SpoilerIt doesn't return anything! You need to add

return textButton
You ought to have tested some other objects as well. :P/>/&amp;gt;

Edit: TextButtons don't handle events properly.
Edit 2: Found the problem dealing with event handling.

Solution, tested, works:
SpoilerOn line 112, you have the following in newTextButton():

if self:collidesWithPoint(e[2],e[3],e[4]) then
However, your textButton object has no such collidesWithPoint function. It probably needs to be

if self.rect:collidesWithPoint(e[2],e[3],e[4]) then
But, again, that brings up another problem. Lets look back at the collidesWithPoint function.
Spoiler
function rect:collidesWithPoint(x,y)
		return x &amp;gt;= self.x and x &amp;lt;= self.w and y &amp;gt;= self.y and y &amp;lt;= self.h
end
The function accepts two variables, not three. When you pass three variables, it will take the first two.

if self.rect:collidesWithPoint(e[2],e[3],e[4]) then
Let's look back at the arguments retrieved from os.pullEvent()…
With a mouse_click event, you get 4 variables
1. "mouse_click"
2. Integer representing which mouse button was clicked (1, 2, or 3, for left, right, and middle, respectively) (yeah, there might be 4, 5, and so on for the other "advanced" mice…)
3. X value of the location clicked on screen
4. Y value of the location clicked on screen

So what you're actually doing is collidesWithPoint([mouse button number], x)

You need to either change your collidesWithPoint to accept a third parameter, or take out the e[2] from your call on line 112.

One last thing.
Pretty sure you added it as a debug, but it's worth pointing out.

Lines 156-162 (line numbers before fixing the previous bugs)
Spoiler
function toggleBackgroundColor(self)
		if self.backgroundColor == colors.red then
				self.backgroundColor = colors.lime
		else
				self.backgroundColor = colors.red
		end

end
Should be
Spoiler
function toggleBackgroundColor(self)
		if self.rect.backgroundColor == colors.red then
				self.rect.backgroundColor = colors.lime
		else
				self.rect.backgroundColor = colors.red
		end

end
darkroom #21
Posted 10 April 2013 - 01:13 AM
Kisses feat :)/> thanks so much everything works great now!