Modify _G.type, cc.expect, etc. with custom types
sorucoder opened this issue ยท 12 comments
As CC:Tweaked has included custom "classes," it would be useful to check types for those classes (as well as user defined ones). I imagine this could work using a __type
metatable field of type string
, and then _G.type
would return that if it has one.
Example:
local currentTerm = term.current()
print(type(currentTerm))
Output:
term.Redirect
I don't think type
's behaviour should change, but we could switch to Lua 5.3+'s behaviour of using using the __name
metamethod in error messages (see the bottom of this manual section.
we could switch to Lua 5.3+'s behaviour of using using the __name metamethod in error messages (see the bottom of this manual section.
I agree with this suggestion. Instead of modifying _G.type
, how about a module that checks for types using the __name
metafield?
type
does not return the name from the metatable's __name
field. The code needs to check against this display name explicitly.
No, that's intentional. __name
is only used for error messages, it shouldn't be used to drive any logic - there's no guarantee that two values with the same __name
are the same type.
No, that's intentional.
__name
is only used for error messages, it shouldn't be used to drive any logic - there's no guarantee that two values with the same__name
are the same type.
I was hoping for a way to detect for things like term.Redirect
for custom libraries and such. Sure, I could check for the methods I need, but that seems to be more work than it would be to just check the __name
field. I wasn't looking for a guarantee per se, just a verification. If some one wants to pull a dirty trick to change the name field, I'd consider that undefined behavior, and going out of one's way to get code to run.
For a concrete example, I would like to do:
function texture:render(parent, ...)
parent = expect(1, parent, "nil", "term.Redirect") or term.current()
-- render the texture to parent...
end
instead of
function texture:render(parent, ...)
parent = expect(1, parent, "nil", "table") or term.current() do
expect.field(parent, "setCursorPos", "function")
expect.field(parent, "blit", "function")
-- whatever else would be needed...
end
-- render the texture to parent...
end
Checking the names is what term.redirect
does. The lazy way for user code to check is to just call redirect so that it does the check and then if you didn't want to switch term object then redirect back to what you were using.
Checking the names is what
term.redirect
does. The lazy way for user code to check is to just call redirect so that it does the check and then if you didn't want to switch term object then redirect back to what you were using.
Okay, but what about http.Websocket
or io.Handle
? Calling pcall
to see if they are what they say they are is even more intense than just checking for needed methods (or at least, more code than necessary).
Returning these objects with the metatable information necessary seems like a trivial feature to implement. Someone HAS to go out of their way to set the type to something identical. And it also does not harm the original functionality.
Also, I just tested that. If passed an empty table, term.Redirect
throws an error stating that it does not have the supported method, and terminates the computer. So, no, that's not the way to go about it.
Okay, but what about
http.Websocket
orio.Handle
? Callingpcall
to see if they are what they say they are is even more intense than just checking for needed methods (or at least, more code than necessary).
Why are you needing to pcall these values? If you run http.websocket
, it returns either a websocket object or false
. You can just do
local ws, err = http.websocket("bla")
if ws then
-- It returned a websocket object
else
printError(err)
end
I'm not seeing why you would need to pcall it.
Edit: Oh, in a library implementation, so you can check the inputted object is what is expected. I understand now.
If some one wants to pull a dirty trick to change the name field, I'd consider that undefined behaviour, and going out of one's way to get code to run.
I think my concern is less people doing dirty tricks, and more that it's not very fool-proof. I think the risk is less people impersonating types, and more two libraries accidentally using the same type name - unless you force people to write the fully qualified type name, that's going to happen :).
An alternative solution here would be to check value's metatable directly (or some equivalent type handle). This is what what we do in cc.pretty
and io
1, and it would be pretty easy to extend cc.expect
to support that more directly. I'm not entirely sure this is the right path either - it's pretty clunky, especially for consumers of these libraries wanting to check types.
Footnotes
-
Not sure why this doesn't use
cc.expect
, it really should. โฉ
Of course, all of this is made much nastier by the fact that CC uses a mixture of nominal and structural types, and there's not really any rule about which one to prefer. I think my vague thoughts are
-
Functions which work with the internal/private structure of an object should use nominal types. Examples here are
cc.pretty
orio
, as they rely on all sorts of implementation details that arbitrary objects won't conform to.The natural follow-on from there, is that anything which calls these functions should also use nominal types for its type checks.
-
Anything which cares about the public API an object exposes, rather than its internal details, should just validate the object has the correct structure. This is mostly what Lua does already (e.g.
table
functions check the object supports indexing and the length operator).We should probably make this sort of thing easier by adding various methods to check an object quacks like it should (e.g.
term.isRedirect
).
SquidDev wrote:
I think my concern is less people doing dirty tricks, and more that it's not very fool-proof. I think the risk is less people impersonating types, and more two libraries accidentally using the same type name - unless you force people to write the fully qualified type name, that's going to happen :).
I'm in favor of writing out a FQDN. After all, we're expected to with require
. So instead of term.Redirect
, http.Websocket
, and io.Handle
, it would be cc.term.Redirect
, cc.http.Websocket
, and cc.io.Handle
.
SquidDev wrote:
Of course, all of this is made much nastier by the fact that CC uses a mixture of nominal and structural types, and there's not really any rule about which one to prefer. I think my vague thoughts are
* Functions which work with the internal/private structure of an object should use nominal types. Examples here are `cc.pretty` or `io`, as they rely on all sorts of implementation details that arbitrary objects won't conform to. The natural follow-on from there, is that anything which calls these functions should also use nominal types for its type checks. * Anything which cares about the public API an object exposes, rather than its internal details, should just validate the object has the correct structure. This is mostly what Lua does already (e.g. `table` functions check the object supports indexing and the length operator). We should probably make this sort of thing easier by adding various methods to check an object quacks like it should (e.g. `term.isRedirect`).
What I would propose is a type registry system in cc.expect
. Then cc.expect
could register the standard library nominal types and structural types. Then, cc.expect.expect
would use the name to either check the metatable field __name
for nominal types, or validate the structure for structure types (both could be done using an internal table that maps strings to functions).
Here's a basic proposal of what I was thinking:
local expect = {} do
-- Registry for nominal and structural types.
-- Keys are the type names.
-- If value is a boolean, the type is checked nominally.
-- If the value is true, the result of _G.type will be checked against.
-- If the value is false, the __name field of the value's metatable will be checked against.
-- If value is a function, the type is checked structurally. The function should take on parameter and return a boolean.
local registry = {}
registry["nil"] = true
registry["boolean"] = true
registry["number"] = true
registry["string"] = true
registry["function"] = true
registry["thread"] = true
registry["userdata"] = true
registry["table"] = true
local function isType(value)
for registryName, registryPredicate in pairs(registry) do
if value == registryName then
return true
end
end
return false
end
registry["type"] = isType
local getPrimitiveType = type
local function getNominalType(value)
if type(value) == "table" then
local metatableOfValue = getmetatable(value)
if metatableOfValue then
return metatableOfValue.__name or "object"
end
end
end
local function expectType(position, type)
local typeOfValue = getPrimitiveType(type)
if typeOfValue ~= "string" then
error(string.format("bad argument #%d (expected type; got %s)", position, typeOfValue), 3)
elseif not isType(type) then
error(string.format("bad argument #%d (unregistered type %s)", position, type), 3)
end
return type
end
local function formatTypeSpecification(...)
local nonNilTypes = {}
local optional = false
for typeIndex = 1, select("#", ...) do
local type = select(typeIndex, ...)
if type ~= "nil" then
table.insert(nonNilTypes, type)
else
optional = true
end
end
local specification = table.concat(nonNilTypes, "|")
if optional then
specification = specification .. "?"
end
return specification
end
function expect.register(name, predicate)
do
local typeOfName = type(name)
if typeOfName ~= "string" then
error(string.format("bad argument #1 (expected string; got %s)", typeOfName), 2)
end
end
do
local typeOfPredicate = type(name)
if typeOfPredicate ~= "nil" and typeOfPredicate ~= "function" then
error(string.format("bad argument #2 (expected function?; got %s)", typeOfPredicate), 2)
end
end
registry[name] = predicate or false
end
function expect.expect(position, value, ...)
do
local typeOfPosition = type(position)
if typeOfPosition ~= "number" then
error(string.format("bad argument #1 (expected number; got %s)", typeOfPosition), 2)
end
end
-- Check types are in registry and check value against type
local gotPrimitiveType = getPrimitiveType(value)
local gotNominalType = getNominalType(value)
for expectedTypeIndex = 1, select("#", ...) do
local expectedType = expectType(expectedTypeIndex + 2, select(expectedTypeIndex, ...))
local registryPredicate = registry[expectedType]
if registryPredicate == true then
if expectedType == gotPrimitiveType then
return value
end
elseif registryPredicate == false then
if expectedType == gotNominalType then
return value
end
elseif registryPredicate(value) then
return value
end
end
-- If we can determine the function name with a high level of confidence, try to include it.
local functionName do
local success, info = pcall(debug.getinfo, 3, "nS")
if success and
info.name and
info.name ~= "" and
info.what ~= "C"
then
functionName = info.name
end
end
if functionName then
error(string.format("bad argument #%d to \"%s\" (expected %s; got %s)", position, functionName, formatTypeSpecification(...), gotNominalType or gotPrimitiveType), 3)
else
error(string.format("bad argument #%d (expected %s; got %s)", position, formatTypeSpecification(...), gotNominalType or gotPrimitiveType), 3)
end
end
end
return setmetatable(expect, {__call = function(_, ...) expect.expect(...) end})