Team Spartan Cookies & Milk Forums
Release Library: Dynamic Type System - Printable Version

+- Team Spartan Cookies & Milk Forums (https://teamscm.co.uk)
+-- Forum: Garry's Mod (/forumdisplay.php?fid=4)
+--- Forum: Starfall (/forumdisplay.php?fid=7)
+--- Thread: Release Library: Dynamic Type System (/showthread.php?tid=2674)



Library: Dynamic Type System - Kosmos - 03-04-2018 05:20 PM

Lua is not an object oriented programming language. This library uses the existing features (tables and metatables) to fix that. What you get is a system of types you can declare at any time, hierarchical packages which contain types, public and private methods or fields and limited metamethod support. In order to remain performance friendly, some of the more complicated features are not (and maybe never will be) present, most notably inheritance.
You can find the file under "lib/struct.txt", it is execute-only and cannot be spawned on it's own. As with all my libraries, requiring them gives you a table containing the library's functions.

General syntax
Names are strings of lower and uppercase letters, digits or underscores which do not start with a digit (sorry ano). This is true for type names and each of the parts in a package name. Packages are hierarchical, separated by a dot. A package or type name must not be empty. The one exception is the root package, it's name is nil and exists only to the package tree has a single root.
Function names and fields have the same restriction as names, except that only a select few names may start with a double underscore, those are reserved for metamethods. You may not declare anything but a function with that name, and only the recognized metamethods may be declared with such a name (you can't make up your own metamethods).
A special case to metamethods are getters and setters. They serve to emulate __index and __newindex, but are declared in a different way.
Packages do not, like types and members, have a visibility. They are always visible, but if they are sealed you need a private reference to be able to create classes or packages inside them. When creating a package, you get it's private reference, when looking it up you don't (this is how the standard namespace is protected).
Types and members have an indication of being public or private. Creating a type only gives you it's public reference, because private references in this context are used to access private members. If you are trying to access something private through a public reference, it will appear to simply not exist.

Metamethod definitions
Metamethods are similar to normal methods, but you cannot explicitly call them like a member, and you also cannot pass away it's private references - that is because they cannot be made private and they can also not be made static (for obvious reasons).
  • __ctor(...) (optional): Called when an instance of this type is constructed. Takes the arguments given to the constructor (all of them, unchecked and unprocessed).
  • __tostr() (optional): Called when an attempt to convert an instance to a string is made. There's a default which returns the type name if not implemented.

Library functions
struct.pack(path)
Obtains a reference to an existing package. Errors if the package does not exist. Returns the public reference, even if the package was created by you.

struct.package(path, sealed)
Similar to the above, this function will create the package if it does not exist. If it does, the package will be returned. Errors if you request a sealed package but the package already exists, or if the package cannot be created because one of it's parents is sealed. Returns the private reference if you created the package, the public one if you didn't.
Note: Parent packages this function creates will never be sealed, only the last element in the chain - the one you actually requested - will be.

struct.subdef(name, sealed, parent)
Creates a single package with the specified name and seal inside the given parent. If the parent is nil, defaults to the root package. Errors if the parent is sealed and you don't have a private reference. You cannot redefine an existing package or type this way.
Note: The above functions take a package path, this one only accepts a single name, which must therefore not contain any dots.

struct.typedef(package, name, proto, private)
Declares a class with the specified name inside the given package. The third argument must be a table with string:table pairs (referenced as name:data) to describe the members:
Methods have data.func (function) to describe the function that is called when the member is. It takes all arguments of the call, except that the first one is transformed into a private reference first.
Metamethods are just like methods, but their name starts with a double underscore. Because anything with a data.func key is parsed as a method, no accessor or field's name may start with a double underscore.
Getters/Setters (Accessors) have data.get (function) and/or data.set (function) which are a hybrid of fields and methods. They are called immediately when someone accesses them. They take a private reference and, in the setter's case, the passed value.
Fields have only an optional data.value which is the default value when constructing an instance, or the starting value of the field if it's static.
All but metamethods have an additional field data.private (boolean) to describe the required access level of the member. It defaults to false for methods and accessors, and true for fields.
All but metamethods have an additional field data.static (boolean) which makes a member associated with the type instead of it's instances. You can still access it through an instance, but you will always reach the type's same member. It defaults to false for all members.
Note: accessors can be only getters or only setters if desired. If someone attempts to run the action that is not valid, they will be greeted with an error.

struct.typeof(ref)
A simple function which returns the full name (package and type, concatenated with a dot) of an instance's type. Returns no value if it is not an instance in this type system.

Library fields
  • root: Public reference to the root package. You don't need this to declare packages (it's the default) but you can use it to traverse the tree.
  • std: Public reference to the standard package, where I'll put some helpful classes when I stop being lazy. You can't create types or packages in here though.

What you might want to know
Miscellaneous stuff that isn't obvious and I haven't mentioned before.
  • Every method, static and instance, receives a first parameter which is a private reference to itself. Similarly, you need to use either the public or private reference to call methods. This makes them look just like those from Lua, syntactially. Whether you use a public or private reference determines your access, so make sure your keys aren't stolen.
  • Try your best to not give away private references! If you have a private reference you can index it with nil to get a public reference (if it's already public you won't do any harm, so be safe if you can't be sure):
    local public = private[nil]
  • The reverse is also possible, if you have the private reference to some type or one of it's instances, you can index it with a public reference of the type or one of it's instances to get the private reference.
  • In order to avoid package naming issues, you should start all packages you own with your name. Make sure that this "name" you pick is unique and doesn't change. Veegi will find this easy, T-19 might want to settle for a single number and the myriad of doctors has to get creative. That's just a suggestion, you can mess up your libraries however much you want really.

Example Usage
That's a big wall of text nice and all, but how do I use it?
local struct = require("lib/struct.txt")
local pack = struct.package("kosmos.example", false) -- unsealed so other libraries can add to it
local tref = struct.typedef(pack, "Atomic", {
__ctor = {func = function(self, arg) self.val = arg end}, -- constructor stores first argument into a field
val = {private = true}, -- private is the default anyway, this holds the atomic variable
-- enables accessing the variable, normally making the field public would be better but this is an example
value = {get = function(self) return self.val end, set = function(self, val) self.val = val end},
__tostr = {func = function(self) return tostring(self.val) end}
})
-- and now to use it
local atom = tref("gross") -- technically works, you should name the variable better
print(atom) -- calls tostring internally, which calls __tostr on the atom
atom = pack.Atomic("less "..atom.value) -- you can access the value
print(tostring(atom)) -- same as above