dot
dot

Reputation: 15670

lua class - can't set property and retrieve value

I'm trying my hand at creating classes in lua. I've written some simple code to set and get a string value, but i keep getting null back as a result.

Here's what my class definition looks like:

appUser = {}
appUser_mt = { __index = appUser }

-- This function creates a new instance of appUser
function appUser:new()
    local new_inst = {}    -- the new instance
    setmetatable( new_inst, appUser_mt)-- all instances share the same metatable.
    return new_inst
end
-- Here are some functions (methods) for appUser:
function appUser:setLastName(lname)
    appUser._lname = lname
end

function appUser:setFirstName(fname)
    appUser._fname = fname
end

function appUser:getLastName()
    return appUser._lname
end

function appUser:getFirstName()
    return appUser._fname
end

return appUser -- do I need this???

My test code that tries to create 2 users looks like this:

require "user"
local a = appUser:new{setLastName="Doe"}
local b = appUser:new{setLastName="Doe-Doe"}
print(a.getLastName())
print(b.getLastName())

When I run the test script from the commandline, this is what I get:

mydevbox:/usr/share/vsx/lib/testapp# lua testuser.lua
nil
nil
mydevbox:/usr/share/vsx/lib/testapp# 

What I've tried so Far:

  1. I've tried changing the way I call the methods from print(a.getLastName()) to print(a:getLastName())

  2. I've changed the variable definitions from:

    appUser._lname to _lname

so that my code look like this:

local _lname
function appUser:setLastName(lname)
    _lname = lname
end

function appUser:getLastName()
    return _lname
end

I'm not sure what I'm doing wrong. Any suggestions would be appreciated. Thanks.

EDIT 1

The following changes have been made to the code to test Etan's answer and mine: 1. I've changed the constructor to accept a parameter as noted in my answer... 2. I've added the reference to "self" in the body of the set/get functions.

-- This function creates a new instance of appUser
appUser = {}
appUser_mt = { __index = appUser }

function appUser:new(new_inst)
    new_inst = new_inst or {}    -- the new instance
    setmetatable( new_inst, appUser_mt)-- all instances share the same metatable.
    return new_inst
end
-- Here are some functions (methods) for appUser:
function appUser:setLastName(lname)
    self._lname = lname
end

function appUser:setFirstName(fname)
    self._fname = fname
end

function appUser:getLastName()
    return self._lname
end

function appUser:getFirstName()
    return self._fname
end

object A and B in the test script still fail with nil, but C works, because I'm not creating it the same way I instantiate a or b. (Please see my answer)

Edit 2

appUser = {}
appUser_mt = { __index = appUser }

function appUser:new()
    return setmetatable( {}, appUser_mt)-- all instances share the same metatable.   
end

local function validate_vmail(vmail_id)
    local success = true    
    if type(vmail_id) ~= 'string' then
        success = false
    end

    if not exists_in_database(vmail_id) then
        success = false

    end
    return success
end 

function appUser_mt.__index(t, k)
  if k == 'lastName' then
    return t._lastName
  end
  return rawget(t, k)
end


function appUser_mt.__newindex(t, k, v)
    local success = false
    if k == 'lastName' then
        k = '_lastName'
        v = v:gsub('^.', string.upper) -- Make sure the first letter is uppercase.
        success = true
    end
    if k == 'vmail_id' then     
        if validate_vmail(v) then
            v = v
            success = true
        else
            success = false
        end
    end
    if success then
        rawset(t, k, v)
    end 
end    
return appUser

And here's the client that instantiates this class:

local a = appUser:new()
a.lastName = "doe"
print(a.lastName)

local b = appUser:new()
b.lastName = "dylan"
print(b.lastName)

a.vmail_id = 1234
print(a.vmail_id)

The code seems to be working, but I'd like to make sure I understood your comments / answers thus far.

Upvotes: 3

Views: 3755

Answers (3)

Ryan Stein
Ryan Stein

Reputation: 8000

Module

You're not assigning the result of require "user" anywhere, so I assume you might have a module call hidden away somewhere. You need return appUser if you intend to use it without calls to module, such as this:

-- mlib.lua
local M = {}
function M.add(a, b) return a + b end
return M

-- script.lua
local m = require'mlib'
print(m.add(2, 2))

Constructor

You can simplify your constructor function.

function appUser.new()
  return setmetatable({}, appUser_mt)
end

I think I understand your intention with the following code, but it's not going to work the way you want it to without a little tweaking. I'll come back to this.

local a = appUser:new{setLastName="Doe"}
local b = appUser:new{setLastName="Doe-Doe"}

Functions

Remember that any time you define a function with :, you're inserting an implicit self parameter as the first argument. The same goes for calling a function as well.

function t:f() end
function t.f(self) end
t.f = function(self) end

All of these lines are equivalent statements.

t['f'](t)
t.f(t)
t:f()

Setters / Getters

Setters and getters are not anywhere as needed as the case may be in other languages. There is no concept of private or public interface to tables in Lua, so if all you're doing is assignment and retrieval, your current setup could become a maintenance liability in the future. There is nothing wrong with doing the following.

local class = {}
class.lastName = 'Doe'

In fact, there are metamethod facilities such that you could intercept certain key assignments and perform operations on the values, if you really wanted setters but also wanted them to blend in with other properties. Let me give you an example.

local mt = {}
function mt.__index(t, k)
  if k == 'lastName' then
    return t._lastName
  end
end

function mt.__newindex(t, k, v)
  if k == 'lastName' then
    k = '_lastName'
    v = v:gsub('^.', string.upper) -- Make sure the first letter is uppercase.
  end

  rawset(t, k, v)
end

local class = setmetatable({}, mt)
class.lastName = 'smith'
print(class.lastName) -- Smith

Constructor, pt. 2

To break this down a little bit, let's look at the following statement.

local a = appUser:new{setLastName="Doe"}

The {setLastName = "Doe"} part is simply constructing a table with a key (or property) named setLastName with the value of "Doe", which are both strings. This is then used as the second argument to the new function of appUser. appUser is the first argument, used as the implicit self parameter mentioned earlier. Remember the :.

That is actually not necessary at all. It's enough to call and define appUser.new as a function without a self, since it doesn't need one. That's all there is to this. new is not yet special in any way that it would somehow know to call setLastName with the parameter "Doe", but we can make it that way.


With all that said, here's an idea of how you could get your constructors to work the way you want.

function appUser.new(options)
  return setmetatable(options or {}, appUser_mt)
end

local a = appUser.new{_lname = 'Doe'}
local b = appUser.new{_lname = 'Doe-Doe'}

However, if you actually wanted to call functions in the constructor table itself, then that requires a little more.

function appUser.new(options)
  local o = setmetatable({}, appUser_mt)

  for k, v in pairs(options)
    -- This will iterate through all the keys in options, using them as
    -- function names to call on the class with the values
    -- as the second argument.
    o[k](o, v)
  end

  return o
end

local a = appUser.new{setLastName = 'Doe'}
local b = appUser.new{setLastName = 'Doe-Doe'}

Please let me know if this has not yet answered your question as completely as possible.

Upvotes: 3

dot
dot

Reputation: 15670

The way I was instantiating the objects is failing...

require "user"
local a = appUser:new{setLastName="Doe"}
local b = appUser:new{setLastName="Doe-Doe"}
print(a:getLastName())
print(b:getLastName())


local c = appUser:new()
c:setLastName("Dylan")
print(c:getLastName())

The results for object "c" show up correctly...

So I guess the problem is with my constructor. i've tried changing it to:

function appUser:new(new_inst)
    new_inst = new_inst or {}    -- the new instance
    setmetatable( new_inst, appUser_mt)-- all instances share the same metatable.
    return new_inst
end

But that hasn't fixed object a or b yet, but I am getting proper results for c

Upvotes: 1

Etan Reisner
Etan Reisner

Reputation: 81032

That changed code looks like it should work correctly however it, like the original, has the problem that it will only ever support a single lname or fname for all appuser instances. You need to use the automagic self variable in your defined functions so that they operate on the passed in instance correctly.

function appUser:setLastName(lname)
    self._lname = lname
end

function appUser:getLastName()
    return self._lname
end

Also this code is not doing what you think it does (and what you want it to do).

local a = appUser:new{setLastName="Doe"}
local b = appUser:new{setLastName="Doe-Doe"}

It is not calling the setLastName function on your new appUser instance. You are creating a table (the {...}), giving that table a setLastName key with the given value Doe or Doe-Doe, and then passing that table to the appUser:new function (which then prompty ignores it as it takes no arguments).

Upvotes: 0

Related Questions