Metatable-based classes
First of all, _M is just a naming convention for a module "namespace". You could name it "module" or "Class" as in your second example.The only real difference is the separation of metatable and "methodtable" / "class" table:
local Class = {}
local metatable = { __index = Class }
function Class.new(...)
...
return setmetatable({ ... }, metatable)
end
cleanly separates the two; the metatable is a local variable and thus kept "private" to the class (if the __metatable field is set, it can further be hidden from getmetatable). If the metatable should be exposed for convenient checks, one way to go would be a metatable field in the Class table:
Class.metatable = metatable
the other pattern just mushes the metatable and methodtable in the same table. Lua's metamethod naming using a double underscore as prefix (e.g. __index) is designed to allow this:
local Class = {}
Class.__index = Class
function Class.new(...)
...
return setmetatable({ ... }, metatable)
end
it requires a circular reference in the Class table (Class.__index); the metatable can be accessed from anywhere as long as the Class table is available because it is the metatable. Downsides of this include (1) potentially worse performance (due to hash collisions, because your metatable contains both the methods and the metamethods) and (2) somewhat dirty: If I inspected your Class table, it won't be neatly separated in metamethods and class methods; I'd have to separate it myself based on method naming.
It also means that indexing instances or the Class table allows "seeing" the underlying implementation which should probably be abstracted away / only accessible through indexing / using the operators / calling: Class.__index and instance.__index will be Class.
Instance checking
Now to the problem of checking whether an object is an instance of a Class: If the second approach is used, comparing the return value of getmetatable works:
if getmetatable(obj) == Class then
-- object is an instance of the class
end
if the metatable is exposed as Class.metatable or the like (may remind you of Object.prototype in JS), this works very similarly:
if getmetatable(obj) == Class.metatable then
-- object is an instance of the class
end
alternatively, the Class may implement it's own isinstance method that checks against the "private" (local) metatable:
function Class.isinstance(table)
return getmetatable(table) == metatable
end
a third alternative to allow the simply comparison against Class would be to set the __metatable field to Class:
local metatable = {__index = Class, __metatable = Class}
that way, the metatable is neatly localized and separated from the Class, yet from the outside it seems as if the "mushy" pattern was used.
If the metatable is however not exposed and no isinstance method is available, you may have to resort to hacks such as the one you have shown to implement this:
if getmetatable(obj).__index == Class then
-- object is an instance of the class
end
this doees however rely on __index being used in a specific way - a hidden implementation detail - and should, if possible, not be used in practice. Suppose __index is changed to an actual metamethod to log a warning because a field has been deprecated:
function metatable.__index(self, field)
if field == "deprecated_field" then warn"deprecated_field is deprecated, use field instead!"
return Class[field]
end
and suddenly your check breaks because the assumption that metatable.__index == Class simply doesn't hold anymore. An alternative hack to consider would be obtaining a metatable from an instance:
local instance = Class(...)
local class_metatable = getmetatable(instance)
...
if getmetatable(obj) == class_metatable then
-- object is probably an instance of the class
end