tech

[English] [Japanese]

12. Class basics

LuneScript supports object-oriented programming through classes.

Class specification

Classes in LuneScript support:

  • access control
  • accessor
  • Inheritance
  • abstract
  • override
  • advertise
  • Mapping
  • interface

Here is a basic class definition.

minimal class definition

A minimal class definition looks like this:

// @lnsFront: ok
class Test {
}

This defines a class called Test.

Note that the class definition must be done in the highest scope.

(2019/6/24) Added support for defining classes within functions. However, exposing classes must be declared at the highest scope.

Public disclosure

To publish a class to an external module, append pub like this:

// @lnsFront: ok
pub class Test {
}

instantiation

Class instantiation uses the new operator as follows:

// @lnsFront: ok
class Test {
}
let test = new Test();

method definition

A method definition is almost the same as a function definition.

// @lnsFront: ok
class Test {
   pub fn func() {
      print( __func__ );
   }
}
let test = new Test();
test.func();  // Test.func

Note that the method cannot be set on the form type.

For example, sub( test.func ) below will error.

// @lnsFront: error
class Test {
   pub fn func() {
      print( __func__ );
   }
}
fn sub( foo:form ) {
   foo();
}

let test = new Test();
sub( test.func );  // error

To avoid this error, create an anonymous function and pass it to sub() like this:

// @lnsFront: ok
class Test {
   pub fn func() {
      print( __func__ );
   }
}
fn sub( foo:form ) {
   foo();
}

let test = new Test();
sub( fn() { test.func(); } );

access control

There are three types of access control:

kinds meaning
pub Public disclosure
local Published in the same module
pro Exposed to subclasses
pri private

If not specified, the default is pri.

self symbol

Inside the method, you can use the self symbol.

The self symbol represents an instance of itself.

In the following example, the public method sub calls the private method func() using self .

// @lnsFront: ok
class Test {
   fn func() {
      print( __func__ );
   }
   pub fn sub() {
      self.func();
   }
}
let test = new Test();
test.sub();  // Test.func

Separation definition

Methods can be defined separately from the class definition.

The method definition above can also be written as:

// @lnsFront: ok
class Test {
}
pub fn Test.func() {
   print( __func__ );
}
let test = new Test();
test.func();  // Test.func

However, it is not possible to define a method of a class defined in another module within the importing module.

prototype declaration

A method definition can also declare only the type within the class definition and separate the actual definition.

The following example prototypes func() and separates the actual definition.

By prototyping func(), it becomes possible to call func() within sub().

// @lnsFront: ok
class Test {
   pub fn func();
   pub fn sub() {
      self.func();
   }
}
pub fn Test.func() {
   print( __func__ );
}
let test = new Test();
test.sub();  // Test.func

Naturally, the type of the prototype declaration and the actual definition of the method must match.

class method definition

A normal method is tied to an instance and cannot be executed without an instance, but a class method tied to a class can be executed without an instance.

Defining a class method is as simple as adding static to the method definition.

// @lnsFront: ok
class Test {
   pub static fn sfunc() {
      print( __func__ );
   }
}
Test.sfunc(); // Test.sfunc

Class methods are only available in classes defined at the topmost scope.

member definition

A member definition is almost the same as a variable definition, with the following differences.

  • Can't set initial value when declaring
  • Added access control
  • accessor can be specified

Here is an example member definition:

// @lnsFront: ok
class Test {
   pri let val1:int;
   pri let val2:int;
   pri let val3:int;
   pub fn func() {
      print( self.val1, self.val2, self.val3 );
   }

}
let test = new Test( 1, 2, 3 );
test.func(); // 1 2 3

Class Test has members val1, val2, val3.

The definition of val1 is pri let val1:int;.

I don't think this is a problem because it's just a normal variable declaration with pri attached.

pri is access control and has the same meaning as the method definition.

mutable

Members and methods also have mutable and immutable.

The differences between methods mutable and immutable are as follows:

  • mutable methods are methods whose members can be changed
  • immutable methods are methods whose members are immutable

Examples of mutable members and methods are shown below.

// @lnsFront: ok
class Test {
   pri let mut val1:int;
   pri let val2:int;
   pub fn func() {
      print( self.val1, self.val2 );
   }
   pub fn add( val:int ) mut {
      self.val1 = self.val1 + val;
   }
}
let mut test = new Test( 1, 2 );
test.func(); // 1 2
test.add( 10 );
test.func(); // 11 2

In this example, val1 is mutable and val2 is immutable. Also, func() is immutable and add() is mutable.

mutable methods declare mut after declaring arguments.

Method add() of mutable sets a value in member val1. This builds without errors.

Then, what happens when the mut declaration of method add() is removed as follows.

// @lnsFront: error
class Test {
   pri let mut val1:int;
   pri let val2:int;
   pub fn func() {
      print( self.val1, self.val2 );
   }
   pub fn add( val:int ) {
      self.val1 = self.val1 + val;  // error
   }
}

The above example will result in an error.

It is an error to attempt to change a member from within a method that is not mutable.

I also get an error if:

// @lnsFront: error
class Test {
   pri let mut val:int;
   pub fn increment() mut {
      self.val = self.val + 1;
   }
   pub fn func() {
      self.increment(); // error
   }
}

In the above example, func() calls increment(), but immutable method cannot call mutable method.

allmut member

As mentioned above, if a member of a class is mutable, but an instance of that class is immutable, the member will be immutable.

The following example accesses the member val of mutable from within the func() method, but since the func() method is immutable, val is also immutable, causing an error.

// @lnsFront: error
class Test {
   pri let mut val:int;
   pub fn func() {
      self.val = self.val + 1;  // error
   }
}

Mutablity is a necessary concept to prevent the value from changing at unintended timing. On the other hand, it's a very strict rule that you can't change any member from a immutable method.

If this rule is applied, the design will become difficult in the following cases, for example.

  • Consider a class Data that manages read-only data associated with a key
  • In the Data class, define a method get() that returns the associated data when a key is given as an argument.
  • Data instance that registers all data to be managed shall be immutable to prevent unnecessary changes.

I think this is a common idea.

Then, as development progresses, you add the following specification:

  • In order to speed up the processing of the above get() method, cache the previous argument key and the data associated with that key

This process of "cache the previous argument key and the data associated with that key" will rewrite the data. That is, it should be mutable, not immutable.

On the other hand, the Data instance is already declared as immutable in many places. In other words, it cannot be cached.

Allmut is used in such cases. allmut declares the member's mutablity to always be mutable, independent of the instance's mutablity.

Here is an example of allmut.

// @lnsFront: ok
class Test {
   pri let allmut val:int;
   pub fn func() {
      self.val = self.val + 1;  // ok
   }
}

By declaring pri let allmut val:int; like this, val will always be mutable. This makes it possible to rewrite val from immutable and func() methods.

However, allmut is only a remedy and should not be overused.

In particular, when doing asynchronous programming in go, which will be described later, safety cannot be guaranteed if allmut is present.

constructor definition

Constructors can be defined in __init.

Constructors differ from method definitions in the following ways:

  • The constructor name must be __init.
  • A return type cannot be specified.
  • Constructor definitions must come after all member definitions.
  • If you inherit from a class, you must run the constructor for that class first.

    • Execution of super class constructor uses super().
  • Constructor must initialize all members.

    • If you don't explicitly initialize a member of a nilable type, it has the initial value nil.
  • You cannot access the methods defined in the class unless all members are initialized in the constructor.

    • However, static methods and super class methods are accessible.
    • Also, the method can be accessed from the function object defined in the constructor.
  • A method with only a prototype declaration cannot be called from within the constructor.
  • You cannot return inside the constructor.

Here is an example constructor:

// @lnsFront: ok
class Test {
   pri let val1:int;
   pri let val2:int;
   pub fn __init() {
      self.val1 = 0;
      self.val2 = 0;
   }
}
let test = new Test();

Note that the constructor can also set initial values for immutable members.

constructor arguments

Constructors can have arguments. This argument is given by the new operator.

// @lnsFront: ok
class Test {
   pri let val1:int;
   pri let val2:int;
   pub fn __init( val1:int, val2:int ) {
      self.val1 = val1 + 10;
      self.val2 = val2 + 10;
   }
   pub fn func() {
      print( self.val1, self.val2 );
   }
}
let test = new Test( 1, 2 );
test.func(); // 11 12

Default constructor

If you don't define a constructor, a constructor is automatically generated to set all members. This constructor is called the default constructor.

The default constructor has arguments to set all members. The order of the arguments is the order in which the members of the class are declared.

The following class declaration does not declare a constructor, so a default constructor is generated internally.

// @lnsFront: ok
class Test {
   pri let val1:int;
   pri let val2:int;
}

Its default constructor is defined as:

// @lnsFront: skip
   pub fn __init( val1:int, val2:int ) {
      self.val1 = val1;
      self.val2 = val2;
   }

The access control of the default constructor is pub.

Default constructor of derived class

There are two types of default constructors for derived classes: the old style and the current style.

present form

The current form default constructor of the derived class Sub is

// @lnsFront: ok
class Test {
   pro let val:int;
}
class Sub extend Test {
   let val2:int;
   pub fn func() {
      print( self.val, self.val2 );
   }
}
let sub = new Sub( 1, 2 );
sub.func(); // 1, 2

Like new Sub( 1, 2 ) above, it will be the argument of the constructor of the super class + all members of the derived class.

old format

An old-style default constructor for a derived class Sub such as

// @lnsFront: ok
class Test {
   pro let val:int!;
}
class Sub extend Test {
   let val2:int;
   _lune_control default__init_old;
   pub fn func() {
      print( self.val, self.val2 );
   }
}
let sub = new Sub( 2 );
sub.func(); // nil, 2

All members of the derived class, like new Sub( 2 ) above.

Note that in the old style, all arguments of the super class must be nilable. You also need to declare _lune_control default__init_old; to use the old-style default constructor. This declaration must be declared after all members have been declared.

Explicit default constructor

A default constructor is generated internally if you don't define a constructor.

But this behavior becomes a problem when you write something like this:

// @lnsFront: error
class Test {
   pri let mut val:int {pub};
   pub static fn create(): Test {
      return new Test( 1 );  // error
   }
}

The above creates an instance of the Test class within the create() method, but an error occurs because the constructor is not declared.

Originally, the default constructor is generated because there is no constructor definition, but the timing of defining the default constructor is done at the end of the class definition, so there is no constructor in the create() method in the class definition.

In such cases, explicitly declare to use the default constructor.

For example:

// @lnsFront: ok
class Test {
   pri let mut val:int {pub};
   _lune_control default__init;
   pub static fn create(): Test {
      return new Test( 1 );
   }
}

By declaring _lune_control default__init;, you can specify the use of the default constructor, and the default constructor will be generated at this timing.

Note that _lune_control default__init; must be declared after all members, just like a normal constructor.

This old-style constructor declaration may be desupported in the future.

destructor

You can define what happens when an instance of your class is released.

Definition method

Define the destructor in the __free() method.

// @lnsFront: skip
class Hoge {
   fn __free() {
      print( __func__ );
   }
}
{
   let hoge = new Hoge();
}
collectgarbage(); // print Hoge

execution timing

Destructors are automatically executed when an instance of the class is released.

Conversely, destructors cannot be called explicitly.

important point

Destructors have a few caveats.

  • When the instance is freed is up to GC
  • Limited Lua versions available

    • lua5.1, not available in fengari
    • access control must be pri

      • pri so override can't
      • cannot be called with super() from the derived destination

class member

Just like methods have class methods, members have class members.

Just add static to the class member definition.

Initialize class members in the __init block.

Here is an example class member definition:

// @lnsFront: ok
class Test {
   pri static let val1:int;
   pri static let val2:int;

   __init {
      Test.val1 = 1;
      Test.val2 = 1;
   }
}

The __init block has the following limitations:

  • Definitions of __init blocks must follow all class member definitions.

    • If you don't explicitly initialize a member of a nilable type, it has the initial value nil.
  • The __init block must initialize all members.
  • Cannot return in __init block.
  • A class method can be called from a __init block, but the class method to be called must be declared before the __init block.

summary

Class definitions in LuneScript support:

  • Define a class with the keyword class
  • instantiation is new
  • Access controllable
  • access itself with the self symbol
  • Class definition and method definition can be separated
  • prototype declaration
  • Class methods, class members in static
  • Constructor is __init
  • If you don't create a constructor, a default constructor will be created.

Next time, I will explain how to create accessors.