Classes - jpbubble/NIL-isn-t-Lua GitHub Wiki

Lua has no support for classes at all, but since Lua has been set up in a way that allows programmers to "cheat" their way around things Lua doesn't support, I could make NIL support classes, which is pretty cool, I think... 😉

Now I'm not fully going into the deep about how classes work under the hood, what I can say is that even in the prototype version of NIL, classes have a bit of a stricter approach than non-class stuff. Cool, huh?

Basic approach:

class MyClass
   int age
   string name
   string gender
end

MyClass kid1
MyClass kid2
MyClass kid3

kid = new MyClass
kid1.name="John"
kid1.age=4
kid1.gender="boy"

kid2 = new MyClass
kid2.name="Cynthia"
kid2.age=5
kid2.gender="girl"

kid3 = new MyClass
kid3.name="William"
kid3.age=6
kid3.gender="boy"

Like most class-based languages, classes are reference based and not value based. Please note they are not just tables. Any kind of field can be added to a table, but classes are a lot more restrictive

kid3.mother = "Joyce"

That will result into an error, since there is no field named "mother" in this class. I need to note that the parser and translator will allow that syntax, and the error is thrown during runtime. Also this will also throw an error.

kid3.age="six"

Since 'age' may only contain a number.

Methods

Now classes also can contain methods. They can just be declared in the same manner as regular functions

class MyClass
 
    int a = 0
    void add(int addwith)
       self.a = self.a + addwith
    end

end

MyClass n
n = new MyClass
for i = 1,10 do
    n.add(1)
    print(n.a)
end

What should be noted is that "self" is always required inside a method (unlike much other languages). What you also may see is that (unlike what is usual in Lua) that you need a period "." and not a colon ":" to call methods. This only works for classes, if you just use a normal table the regular notation still applies.

Static fields

Static fields share their value with all variables of the same class.

class Test
    int a 
    static int b
end

Test T1 = new Test
Test T2 = new Test
t1.a=1
t2.b=2
t2.a=3
t2.b=4
print(t1.a,t1.b) // output: 1 4

Now in the example above you can also do this

print(Test.b) // output: 4

Static functions/methods

Yes, this works too! Contrary to normal methods, they will not have the variable 'self' at their disposal and they can be called from both class variables as from the classes themselves.

class StFunc
    static void MyFunc(string hello)
        print(self) // does work, but will output 'nil'
        print(hello)
    end
 
    void MyMethod(string hello)
        print(self) // does work, now outputs the class var's reference
        print(hello)
    end
end

StFunc.MyFunc("Yo!")
StFunc F
F = new StFunc
F.MyFunc("Hi")
F.MyMethod("Hello")

Output:

nil
Yo!
nil
Hi
<whatever Lua uses for reference data>
Hello

Constructors

Yes, NIL has support for constructors. Please note, NIL is not as sophisticated as C++ or C# on this one, so overloading is not supported (Lua simply won't allow that), but it's dead easy. Just declare a non-static void method called "CONSTRUCTOR", and the best part is, it does allow parameters too.

class Demo

     string myvar

     void CONSTRUCTOR(string something)
            self.myvar = something
            print("Hey you created a class! Cool!")
     end
end

Demo D
D = new Demo("Jeroen")
print(D.myvar)

Output:

Hey you created a class! Cool!
Jeroen

Read-Only field variables

These can be declared by putting the keyword 'readonly' prior to the variable declaration, and then a field variable can only be defined in the class declaration itself, or during while the CONSTRUCTOR is running during class creation (please note, although it's technically possible to call a constructor method after creation too, readonly fields are then not assignable anymore).

class TestReadOnly
     int a
     readonly int b

     void CONSTRUCTOR(a,b)
        self.a = a
        self.b = b // legal now, since we are in the constructor stage.
     end
end

TestReadOnly TRO
TRO = new TestReadOnly(1,2)
print(TRO.a,TRO.b) // outputs: 1  2
TRO.a = 3 // all fine!
TRO.b = 4 // causes an error!

Private and public elements of classes

By default all elements of a class are public, and thus freely accessible, however you may not always want that. Private elements are only accessible from within methods, but not by the scope outside the class.

There are two ways to define a private. Either by prefixing the element's name with an underscore, or with the keyword private


class ClassWithPrivates
  
      int publicint = 10
      int _privateint1 = 20
      private int privateint2 = 39

      void expose()
        // Called from within a method, so no issues here!
        print(self.publicint,self._privateint1,self.privateint2)
      end

end

var CWP
CWP = new ClassWithPrivates
CWP.expose() // Works, privates only accessed by the method

print(CWP.publicint) // Works, this one is public
print(CWP._privateint1) // Error! Underscore, thus private!
print(CWP.privateint2) // Error! Explicitly declared as private, this private!

"get" and "set"

NIL has these two keywords to create "properties" or variables that are actually methods, but disguised as variables. get and set can be put in a variable declaration turning them into 'properties' and the next lines can just be handled like a regular method.

class prop_class

    get int myprop
         return 5
    end

    set int myprop
        if value~=5
           print("You thought you could set the value to "..value..", eh! Bad luck, I'll keep it at 5")
        end
    end

end

var p
p = prop_class()
p.myprop = 7 // output: "You thought you could set the value to 7, eh! Bad luck, I'll keep it at 5"
print(p.myprop) // output: 5

"set int myprop" could basically be seen as "void myprop(int value)" difference is that value is now defined by the = symbol and not by (). "get int myprop" can be seen as "int myprop()". Also the ability to use "self" within these properties will depend whether these properties are "static" or not.

.NEW() static method notice

In NIL versions older than 19.07.13 you were required to create new classes with the .NEW() method like this:

a = MyClass.NEW()

From version 19.07.13 you have to do it like this:

a = new MyClass

The .NEW() method still works, but has been deprecated since version 19.07.13, and may even be removed in version 21.xx.xx or later. Some of my older code may still contain .NEW() and that's why it's still there, but the usage of .NEW() is highly discouraged.

() with 'new' or not

That depends... if you have a CONSTRUCTOR that doesn't take any arguments then () is optional so these will both work!

a = new MyClass
b = new MyClass()

When the CONSTRUCTOR does take arguments then the () are required.

Using NIL classes in 'pure' Lua

Yes, that is entirely possible. Of course, creating and defining the class itself must be done in NIL, but once created pure Lua will be able to handle the entire class as it should, the only difference is that the keyword 'new' cannot be used anymore and that () will be required when creating a new entry.

myclass.nil:

class myclass
    int a
    int b
    int c
    void CONTRUCTOR(aa,ba,ca)
      self.a=aa or 0
      self.b=ba or 0
      self.c=ca or 0
    end
end

myclasstest.lua

NIL.Use("myclass")
test1 = myclass() -- no parameters
test2 = myclass(10,20,30) -- parameters
print(test1.a,test1.b,test1.c) -- outputs  0  0  0
print(test2.a,test2.b,test2.c) -- outputs 10 20 30

Why is "new" required in NIL and not in Lua?

First of all Lua doesn't have support for the "new" keyword. Second just typing a class name, like you can do in Lua would in NIL create a delegate function which is in pure Lua only possible by using the "function" keyword. The "new" keyword gets that conflict away from NIL, that's all.

Destructors

Another important thing about classes are destructors. Like constructors are called when memory is being allocated for a new class variable, destructors are called on the moment they are wiped from the memory. Lua uses a garbage collecting system. All data memory allocated by Lua will automatically be released whenever no variables or anything are assigned to that memory anymore. However, this only goes for data allocated by Lua itself, and not for the data underlying APIs called by Lua have created and managed. And sometimes you do want that stuff to be released as well or memory leaks can be inevitable. Destructors will help you to make that happen. Now creating destructors is as easy as constructors.

class MyClass

    void CONSTRUCTOR()
        print("Yo, guys! I've been created!")
    end

    void DESTRUCTOR()
        print("OH NO! I've been destroyed!")
    end

end

A few notes about destructors

  • There is no telling when the destructor is being called. Lua does not release the memory immediately, but at the time it deems appropriate. This is done for performance reasons, as your program would otherwise slow down. The destructor is not called until Lua has really decided the time is right to clean your trash up.
  • The DESTRUCTOR method must be a non-static void.
  • You can declare parameters to a DESTRUCTOR, but they will all be ignored, so save yourself the trouble.
  • The keyword "private" has no effect at all on a DESTRUCTOR when it comes to garbage collection. You may want to set it to private to prevent it from being called directly causing a big mess in the process.
  • Of course DESTRUCTORS have access to the "self" variable. Should go without saying, but I'll just say it again to prevent confusion.

An important note about "self"

Right now you are always obligated to prefix members of classes with "self" when you call them in any way from the class itself. That was because it was a bit hard on me to make that done automatically, like in many class based languages, such as C# (where "this" is the equivalent of "self", but it's not required all the time). However I cannot rule out this will happen in the future and then your code can really do odd things when you do this:

int h = 4

class MyClass
   int h = 5
   void sh()
      print(h)
   end
end
var mc
mc = new MyClass()
mc.sh()

In the current versions of NIL, this will show '4', since it will take the 'h' variable from the ground scope, however if I manage to make 'self' no longer required in classes it will show '5' as that is then the value to follow. If you wanna prevent bugs with future versions, I recommend you not to do what I did in the example above.

One final note about classes

Classes are one of the very few things in NIL in which NIL linked some of its own code into the translation of your script in stead of pure Lua. For line numbers shown in errors and tracebacks that shouldn't matter as this code is not put in the translation itself. Only a function linking to that. This was unavoidable as Lua would otherwise not support classes the way NIL provides the functionality. This does mean that classes can cause slowdown compared to 'pure Lua'. Nothing serious, but still. Under normal usage nobody should notice, but if speed is your first concern, and every nanosecond counts (in that case you shouldn't be using Lua at all, though) one should be careful with them. A good tip in general "var" is always faster than any other datatype (not counting 'void'). Particularly in classes this can be a way to speed things up, a little.