Class method definitions help enforce strict typing rules to ensure that the method parameter and return values contain the expected types. Note that including strict typing rules is entirely optional. Still, this feature can help alleviate early bugs and improve the development process while defining a strict contract for how other developers should use your code.
The data
argument should be ignored when creating strict typing rules as it will always be the same table value.
local obj = MayronObjects:GetFramework();
local MyPackage = obj:CreatePackage("MyPackage");
local MyClass = MyPackage:CreateClass("MyClass");
MyPackage:DefineReturns("number");
function MyClass:GetTimeRemaining(data)
return data.timeRemaining;
end
MyPackage:DefineParams("number");
function MyClass:SetTimeRemaining(data, timeRemaining)
data.timeRemaining = timeRemaining;
end
local instance = MyClass();
instance:SetTimeRemaining(60);
local timeRemaining = instance:GetTimeRemaining();
print(timeRemaining); -- prints: 60
-- This results in an error:
instance:SetTimeRemaining("60"); -- not a number
A package contains information relating to entities and method definitions. Therefore, declaring parameter and return value types must be done using the package object.
You need to include strict typing rules using DefineParams
and DefineReturns
, as shown on lines 5 and 10, straight before declaring the method. Otherwise, the package may assign the rules to the wrong method.
Line 5 states that the return value must be of type "number"
. The developer who created the class must comply with their promise by returning a number value.
Line 10 states that the first argument passed to the SetTimeRemaining
function must be of type "number"
. Any developer using the class must comply with this enforced rule; otherwise, MayronObjects will trigger an error as illustrated on line 21.
You can also declare that any value should be returned, as long as it is not nil:
MyPackage:DefineParams("any");
function MyClass:SetTimeRemaining(data, timeRemaining)
data.timeRemaining = timeRemaining;
end
4.1 Static Class Method Definitions
Static class functions can also be assigned strict typing rules to specify parameter and return value definitions in the same way as private and public instance methods:
MyPackage:DefineParams("any");
function MyClass.Static:SetTimeRemaining(timeRemaining)
self.TimeRemaining = timeRemaining;
end
MyClass.Static:SetTimeRemaining(60)
print(MyClass.Static.TimeRemaining); -- prints: 60
In the above example, the only difference compared to the example shown previously using a public instance method is that the TimeRemaining
property will be accessible from the class's Static
table. Static functions and properties should only be assigned to the Static
table and not the class itself.
4.2 Optional Method Parameters and Return Types
Additionally, you can declare optional parameter arguments and return values by adding a question mark ?
character in front, or at the back, of the definition type. The recommended convention is to declare optional parameters at the end of the declaration list after any non-optional parameter types (similar to python code conventions) but this is optional:
MyPackage:DefineParams("string", "number", "?table");
MyPackage:DefineReturns("any", "function", "?boolean");
If you prefer adding ?
to the back of the definition type then this is also acceptable:
MyPackage:DefineParams("string", "number", "table?");
MyPackage:DefineReturns("any", "function", "boolean?");
4.3 Variable List Definitions
MayronObjects lets you specify strict typing rules for variable argument lists, also known as varargs, passed to methods. These rules also work for multiple return values. All you need to do is include ...
in front of the definition type. This definition type is referred to as a variable list definition for the sake of simplicity.
The example snippet below enforces the rule that any values passed to the method after the initial string value should only be number
values. Any arbitrary number of return values should also be number
values. You do not need to pass any values to a vararg, ore return any values, when using a variable list definition. These values are always optional but if you do pass some then they must match the definition type.
MyPackage:DefineParams("string", "...number");
MyPackage:DefineReturns("...number");
function MyClass:Execute(data, message, ...)
return ...;
end
Below is an example of values being passed to the method using this strict typing rule:
-- this is allowed:
instance:Execute("hello", 1, 2, 3, 4);
-- this is NOT allowed:
instance:Execute("hello", 1, 2, "3", 4);
-- this is allowed (values are always optional):
instance:Execute("hello");
ALthough you do not need to pass values to a vararg, or return multiple values, when using a variable list definition, if you return an explicit nil
value then this rule is violated.
An "explicit" nil
value means that you have explicitly passed nil
to a method, or returned nil
from a method (e.g., f(nil)
). An implicit
nil is when you do not pass in anything (e.g., f()
), and so a parameter or variable that was supposed to be assigned a return value remains equal to nil
.
The entire variable list is optional but individual values are not. If you want the values inside the vararg, or the multiple return values to be optional (i.e., allow for nil
values to be included in the list at any position), then you must use the optional ?
inside the definition:
MyPackage:DefineParams("string", "...?number");
MyPackage:DefineReturns("...?number");
function MyClass:Execute(data, message, ...)
return ...;
end
-- this is allowed:
instance:Execute("hello", 1, nil, 3, 4);
-- this is also allowed:
instance:Execute("hello");
You cannot declare another definition type after specifying a variable list definition, as this must be the last definition supplied. This rule is similar to how a vararg
parameter must be the last parameter of a function's signature:
-- this is NOT allowed:
MyPackage:DefineParams("string", "...number", "table");
Finally, a variable list definition can be declared as a union type, meaning that any value in the list can be one or more types as specified by the definition:
-- this is allowed:
MyPackage:DefineParams("string", "...number|table");
-- this is allowed:
MyPackage:DefineParams("string", "...number|?table");
-- this is allowed:
MyPackage:DefineParams("string", "...?number|table");
If you mark any of the definition types in the union variable list definition as optional (?
), then explicit nil
values can be used. Lines 5 and 8 above state that any value in the vararg
can either be a number
, table
, or nil
.
4.4 Object and Widget Types
When MayronObjects validates the type of an object it uses the GetObjectType()
method. All objects created with this package inherit the GetObjectType()
method from the base Object class (see section 11). Because Blizzard widgets (such as Frame, Button, Texture, etc.) also have a GetObjectType()
method, you can also use MayronObjects to test for both the type of object created from a package and the type of widget.
MyPackage:DefineParams("Frame", "Button", "?MySlider");
MyPackage:DefineReturns("Handler", "MyClass", "?SomeWidget");
4.5 Parent Classes and Interfaces
If a class implements an interface (see section 8), you can use the interface name as the type of object to see if an object passed to the method call implements the given interface. Similarly, suppose a class has a parent class (see section 7). In that case, you can use the parent class type as a parameter definition, and any child class from any level (if the parent class also has a parent, etc.) will pass the validation test.
local Parent = MyPackage:CreateClass("Parent");
local Child = MyPackage:CreateClass("Child", Parent);
local SomeClass = MyPackage:CreateClass("SomeClass");
-- an interface:
-- see section 8, "Creating an interface" for more info
local IComponent = MyPackage:CreateInterface("IComponent", { ... });
local Component = MyPackage:CreateClass("Component", nil, "IComponent");
MyPackage:DefineParams("Parent", "IComponent");
function SomeClass:Foo(obj)
-- do stuff
end
local someInstance = SomeClass();
local child = Child();
local component = Component();
someInstance:Foo(child, component); -- this is allowed
4.6 Default Primative Parameter Values
Suppose you provide nothing or a nil
value to a parameter, and the strict typing rules have specified a default value for that parameter. In that case, the parameter's value will default to that default value. For example, if you call the Print
method in the below code snippet without any arguments, then message
will be equal to the default "foobar"
value and value
will be equal to the default 14
value:
MyPackage:DefineParams("string=foobar", "number=14");
function MyClass:Print(data, message, value)
print(message, value);
end
Of course, by calling this method with two non-nil values, the default values are ignored. You do not need to include the optional syntax (?
) at the front of declared value types if you have also specified a default value.
MayronObjects lets you also specify default values for variable list definitions, as well as union type variable list definitions:
MyPackage:DefineParams("...number=14");
function MyClass:PrintNumbers(data, ...)
print(...);
end
However, you can only define one default value for union types. In the example below, "foobar" will never be used. It is not possible for MayronObjects to know which value the nil
was supposed to be (i.e., the value might have been either a number or a string value) so the first default value will always be used if nil
is supplied:
MyPackage:DefineParams("...number=14|string=foobar");
function MyClass:PrintNumbersAndStrings(data, ...)
print(...);
end
Therefore, the above snippet is functionally equivalent to:
MyPackage:DefineParams("...number=14|string");
function MyClass:PrintNumbersAndStrings(data, ...)
print(...);
end
4.7 Default Complex Parameter Values
The above approach in section 4.5 works well for simple values parsed from a string, but more complicated parameter types are challenging to describe inside a string, such as widgets/frames, tables, and functions.
For this, use the following approach:
local myDefaultTable = { msg = "foobar" };
MyPackage:DefineParams({"table", myDefaultTable});
function MyClass:Print(data, tbl)
print(tbl.msg);
end
The above code snippet will print "foobar" if you omit to supply another table as the first argument when calling the Print
method; else, MayronObjects will use the default table instead.
You can also use this syntax for default primative types as an alternative for the syntax expressed in section 4.5:
-- these are functionally equivalent:
MyPackage:DefineParams("number=14");
MyPackage:DefineParams({"number", 14});
MayronObjects lets you also specify default complex values for variable list definitions, as well as union type variable list definitions:
MyPackage:DefineParams({"...number", 14});
function MyClass:PrintNumbers(data, ...)
print(...);
end
As previously mentioned, you can only define one default value for union types. In the example below, the table containing msg = "foobar"
will never be used. It is not possible for MayronObjects to know which value the nil
was supposed to be (i.e., the value might have been either a number or a table value) so the first default value will always be used if nil
is supplied:
-- for union types (obj = the framework, see section 13):
MyPackage:DefineParams({"...number|table", 14, { msg = "foobar" }});
MyPackage:DefineReturns({"...number|table", 30, { msg = "foobar" }});
function MyClass:PrintNumbersAndTables(data, ...)
for _, value in obj:IterateArgs(...) do
if (obj:IsTable(value)) then
print(value.msg); -- will always be "custom"
elseif (obj:IsNumber(value)) then
print(value); -- will be 10 or 14
end
end
return ...;
end
local v1, v2, v3 = instance:PrintNumbersAndTables(10, nil, { msg = "custom"});
assert(v1 == 10);
assert(v2 == 14); -- will be the default value from parameter definition
assert(v3.msg == "custom");
Therefore, you only need to specify one default value and the default value can be assigned to any type in the union type (either the number or the table):
MyPackage:DefineParams({"...number|table", 14});
MyPackage:DefineReturns({"...number|table", { msg = "foobar" }});