Microlight icon indicating copy to clipboard operation
Microlight copied to clipboard

No way to fail from an initializer in class

Open andrewstarks opened this issue 12 years ago • 7 comments

Consider:

local ml = require'ml'
local Class = ml.class
tostring = ml.tstring
local My_Class = Class()

function My_Class:_init(msg)
    self.salt = "For the Win!"
    if type(msg) ~= "table" then
        return nil, "need a table with arguments to process for this class!"
    end
    --do some stuff...
    self.butter = "America's Food Sauce!"
    return self
end
local x, err = My_Class("i'm not smart")

print(x, err)
--> {salt="For the Win!"}   nil

Using ml.class, how can it be made possible to fail an initialization? That is, I wish that I could say "if this jackal doesn't pass me what I need to make the object, return nil, errmsg"

As it is, I have to do a separate test, post instantiation to see if the object got made or not (by checking some kind of object flag). I read the documentation, but couldn't find the answer. I think I could patch ml.class to do it, but didn't know if I was missing some obvious design decision that I'm not following...

Thanks!

-Andrew

andrewstarks avatar Feb 07 '13 16:02 andrewstarks

Hi Andrew,

Well, the 'traditional' OOP languages avoid this problem by just throwing an exception. OK, we could do this, but it's not such a Lua friendly thing to do. Perhaps the trouble is that we tend to do too much in constructors. If the constructor doesn't do much, then a method to set the state can be defined that does actually return nil, error. I know, it's a question of style, but having a My_Class(str) call return nil would be odd.

stevedonovan avatar Feb 09 '13 14:02 stevedonovan

I won't disagree. (Btw, I switched to using penlight. I like the Author of that package more. ;)

My case might be odd, but it doesn't feel that way... Yet.

My constructor accepts a table, which contains the arguments for creating an XML element. If passed bogus data (no name given for the element) I want to return:

nil, "Elements must have names."

If they pass nil to it, instead of the argument table, the initializer assumes that the element is "unset" and merrily carries on with empty values.

As it is, I just have a _status field and set it to false, but I have to check it.

I may try patching penlight so that if "_error" field is set to true by the initializer, it looks for "_errormsg" and returns that, after nil.

Or...

If my initializer returns nil and a string as the second return value, I'll assume an error.

I can see how you'd want to keep things generic and that changing this behavior might break other people's code. Also, I am inexperienced enough that even though the possibility of an initializer failing doesn't seem like a bad idea, it might actually be one.

Your reputation is such that a short explanation of why is probably enough for me to consider a different approach.

Thank you!

-Andrew Starks Tightrope Media Systems (612) 840-2939

"As we get older, and stop making sense" -David Byrne

On Feb 9, 2013, at 8:15, Steve J Donovan [email protected] wrote:

Hi Andrew,

Well, the 'traditional' OOP languages avoid this problem by just throwing an exception. OK, we could do this, but it's not such a Lua friendly thing to do. Perhaps the trouble is that we tend to do too much in constructors. If the constructor doesn't do much, then a method to set the state can be defined that does actually return nil, error. I know, it's a question of style, but having a My_Class(str) call return nil would be odd.

— Reply to this email directly or view it on GitHubhttps://github.com/stevedonovan/Microlight/issues/2#issuecomment-13331443..

andrewstarks avatar Feb 09 '13 14:02 andrewstarks

Also.. I'd like to cover the case of dumb input, not just missing input. In my case, an element with two attributes with the same name or the wrong type used for an element name, etc.

Sorry for the noise. :)

-Andrew Starks Tightrope Media Systems (612) 840-2939

"As we get older, and stop making sense" -David Byrne

On Feb 9, 2013, at 8:15, Steve J Donovan [email protected] wrote:

Hi Andrew,

Well, the 'traditional' OOP languages avoid this problem by just throwing an exception. OK, we could do this, but it's not such a Lua friendly thing to do. Perhaps the trouble is that we tend to do too much in constructors. If the constructor doesn't do much, then a method to set the state can be defined that does actually return nil, error. I know, it's a question of style, but having a My_Class(str) call return nil would be odd.

— Reply to this email directly or view it on GitHubhttps://github.com/stevedonovan/Microlight/issues/2#issuecomment-13331443..

andrewstarks avatar Feb 09 '13 15:02 andrewstarks

Well, I shall think about this. It feels like a change-of-policy point - I know we don't have to follow existing models slavishly! From a Lua point of view, makes perfect sense for any function (incl. a constructor) to return nil,error in time-honoured way. And (just to make things entertaining) what goes for Microlight also goes for Penlight ;)

stevedonovan avatar Feb 09 '13 16:02 stevedonovan

Here's a solution; I think this little function try is a good candidate for inclusion in ml and Penlight:

function try (fun,...)
    local ok,r1,r2 = pcall(fun,...)
    if not ok then
        return nil,r1
    else
        return r1,r2
    end
end

local ml = require 'ml'

A = ml.class()

function A:_init (a)
    if a > 10 then error("a is too large!") end
    self.a = a
end

print(try(A,11))

--~ nil try.lua:15: a is too large!

Can be generalized to handle an arbitrary number of return values - this at least catches functions normally returning nil, error. Might also want to strip out file:line info.

stevedonovan avatar Feb 14 '13 09:02 stevedonovan

As a wrapper around pcall? not so much. However, when one contemplates the use of a global flag, such as DEBUG or RELEASE, then try the method would be excellent and I think it should absolutely be included (and generalized, as you've pointed out.) It's particularly useful when attempting socket connections or other unknown and likely-to-fail things.

Along with try, one might consider trycatch:

function try (fun,...)
    local ret_vals= {pcall(fun,...)}
    if not ret_vals[1] then
        return nil,ret_vals[2]
    else
        return table.unpack(ret_vals)
    end
end

function trycatch (catchfunc, fun,...)
    local ret_vals= {pcall(fun,...)}
    if not ret_vals[1] then
        return catchfunc({fun, ...}, ret_vals)
    else
        return table.unpack(ret_vals)
    end
end
---[[Tests
local ml = require 'ml'
A = ml.class()
function A:_init (a)
    if a > 10 then error("a is too large!") end
    self.a = a
end
function A:__tostring() return tostring(self.a) end

print(try(A,11))
print(trycatch(
    function(args, ret_vals) 
        args[2]  = 10 
        return args[1](args[2])
    end, A, 11
    ))
--> nil  try.lua:24: a is too large!
--> 10
--]]

I think it may serve a slightly different purpose than the intended effect of adding the ability for a class constructor to fail with an error.

If I understand your point:

In a language without type checking, it makes less sense for the assignment of a variable, or in this case a class constructor, to fail, as it would in C++ or C# where interfaces must be defined before they may be used. Therefore, let the consumer of the package check for validity on their end. If an error must be thrown, then provide the try mechanism here.

I think that is reasonable and I'm still left wanting to patch the class library to pass "nil, msg" if my return value is such. It seems more like Lua to me, although your position has merit and I've been happily using PL without my patch, providing more evidence that the status quo is the way.

My hubris is itching. :)

andrewstarks avatar Feb 14 '13 17:02 andrewstarks

Btw, the interface for my trycatch example could be better. Please don't judge me, based on that. :)

andrewstarks avatar Feb 14 '13 17:02 andrewstarks