Microlight
Microlight copied to clipboard
No way to fail from an initializer in class
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
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.
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..
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..
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 ;)
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.
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. :)
Btw, the interface for my trycatch example could be better. Please don't judge me, based on that. :)