User defined types - Palamecia/mint GitHub Wiki

User-defined types

User-defined types are custom types that allow for the description of custom values, data structures, and behaviors. There are two kinds of user-defined types:

Once defined, these types can be used like other built-in types.

Note

If a type is defined within a control structure, the type will be effectively defined only if the definition script is executed.

Enumerations

Enumerations are the simplest type definitions, providing only a list of global constant values.

The definition of an enumeration is introduced by the enum keyword followed by a name corresponding to the type name. The definition opens a block of constant definitions.

enum MyEnum {
}

Constants are defined within the block using a list of symbol names. The = operator can follow the name to set the value of the constant. The value must be a number. If no value is given, a value is automatically set by adding one to the previous value inserted. If no previous value was inserted, 0 is used.

enum MyEnum {
    first       // gives 0
    second = 5  // gives 5
    third       // gives 6
}

Tip

The is operator can be used to check if the value of a variable is a constant of an enumeration.

Classes

Classes describe a list of member symbols that will be associated with each instance of the class. Each member has a visibility and a value.

A class definition is introduced by the class keyword followed by a name corresponding to the type name. The : operator can optionally be added to introduce a list of base classes. The definition opens a block of member definitions.

class MyClass {
}

Members visibility

Members visibility modifiers can be added before access modifiers. They change the accessibility of the member depending on the context. By default, each member can be accessed from anywhere.

The following visibility modifiers can be added to any member:

  • + (public): The member can be accessed from anywhere. This is the default.
  • # (protected): The member can only be accessed from inside the class or from a derived or base class.
  • - (private): The member can only be accessed from inside the class.
  • ~ (package): The member can only be accessed from the same package.
class MyClass {
    publicMember
    + otherPublicMember
    + const constPublicMember
    ## protectedMember
    - privateMember
    ~ packageMember
    - @privateGlobalMember
}

Important

Members with a global access modifier initialized to none have a special behavior. They can be accessed from anywhere regardless of the visibility modifier used until their value is initialized.

Members value

Each member of a class has a value. The = operator can follow the member name to initialize this value. If no value is given, the none value is used.

class MyClass {
    member1 = 0
    member2 = 'text'
    member3 = false
}

The member definition block has no context. Therefore, no variable or user-defined type can be used to initialize a member. The null value can then be used as a placeholder before the initialization of the member in the constructor.

Note

It is recommended to use the null value for members that will be initialized later, like in a constructor or in an update method, and the none value for optional members like callbacks or handlers.

Values from built-in type

Any built-in type can be used as a member value. However, as the member definition block has no context, some special conditions apply to some types:

User defined member types

Classes can also contain other user-defined types as global members. Those types can use the visibility mechanism and are introduced by the corresponding keyword.

class MyClass {
    enum MyEnum {}

    class MyChildClass {}

    - class MyPrivateClass {}
}

Methods

Members using the function type are called methods. Non-global methods must have at least one parameter. This first parameter will be used to store the object instance.

Note

By convention, the first parameter of a method is named self, but any name can be used.

As a function value can have several associated blocks, the += operator can be used to add a block to a function member.

class MyClass {
    method = def (self) {}
    method += def (self, value) {}
}

The def keyword can also be placed before the member name to create a method. In this case, the method will be automatically created or extended.

class MyClass {
    def method(self) {}
    def method(self, value) {}
}

Note

By default, methods are re-bindable. Which mean, the associated function can be changed on the instance using the = operator. To make methods non re-bindable, they should be declared const.

class MyClass {
    const def method(self) {}
}

Constructor and destructor

Classes can have two special members: new and delete.

The new member is called the constructor. When a new instance of the class is created, this member is called. The returned value is then used as the new instance.

The call parameter list of the constructor starts with the instance of the object being created. This instance is only initialized with the values associated with the class members. Additional parameters can follow and must then be passed when the class is instantiated.

class MyClass {
    def new(self, value) {
        self.value = value
        return self
    }

    - value = null
}

Note

If the constructor is not defined, the class instantiation gives an object only initialized with the values associated with the class members.

Classes also provides a global allocate method that create an object instance only initialized with the values associated with the class members. The constructor is not called by this method. This is useful to create named constructors where the default constructor is not suitable.

class Result {
    def new(self, value) {
        self.value = value
        return self
    }

    @def fromError(error) {
        let self = Result.allocate()
        self.error = error
        return self
    }

    - error = none
    - value = none
}

As this method is defined for any user defined classes, it can also be called as an instance method to allocate the real type of an object.

class Foo {
    def clone(const self) {
        let other = self.allocate()
        // ...
        return self
    }
}

class Bar : Foo {
    // ...
}

let foo = Foo()
let bar = Bar()

let otherFoo = foo.clone()  // `otherFoo` is an instance of `Foo`
let otherBar = bar.clone()  // `otherBar` is an instance of `Bar`

Note

This class method has a protected visibility. So it can not be called outside of the class hierarchy.

The delete member is called the destructor. When an instance is destroyed, this member is called. The returned value is ignored.

The destructor can take only one parameter corresponding to the instance of the object being destroyed. Some final operations can then be processed on the object.

class MyClass {
    def new(self) {
        if MyClass.user is null {
            MyClass.user = self
            return self
        }
    }

    def delete(self) {
        if MyClass.user is self {
            MyClass.user = null
        }
    }

    - @user = null
}

Caution

Exceptions can not be raised from a destructor. In such cases, the interpreter will immediately stop with an error.

Operator overloading

Some operators can be overloaded by a class. To override the default behavior of an operator, a special member can be added. These members are introduced by the def keyword followed by the name of the operator to overload. They use a call parameter list with an amount of parameters depending on the overloaded operator.

def MyClass {
    def +(self) {}
    def +(self, value) {}

    def [](self, key) {}
    def []=(self, key, value) {}

    def ()(self, a, b, c) {}
}

Inheritance

After the class name, the : operator can be added to insert a base class list. The base class list contains the names of the classes to extend separated by ,.

class MyBase {
}

class MyOtherBase {
}

class MyClass : MyBase, MyOtherBase {
}

Note

A class can also extend an enumeration, but built-in types cannot be extended.

Each non-global member of the extended classes will then be copied to the newly created class with their values and modifiers, except for members already defined in the class.

class MyBase {
    member1 = 1
    member2 = 2
}

class MyClass : MyBase {
    member1 = 5  // member1 gives 5
                 // member2 gives 2
}

Important

When a class extends several classes that contain members in common, those members must be redefined in the new class to resolve them.

Members can also be defined with the final or override modifier to change the behavior when a member is redefined in the child class.

A member defined with the final modifier cannot be redefined in child classes.

class MyBase {
    final member = 1
}

class MyClass : MyBase {
    member = 5  // error: member 'member' overrides a final member of 'MyBase' for class 'MyClass'
}

A member defined with the override modifier must be already defined in at least one base class.

class MyBase {
    member1 = 1
}

class MyClass : MyBase {
    override member2 = 5  // error: member 'member2' is marked override but does not override a member for class 'MyClass'
}

A method defined with the override modifier also requires that the child class redefine each signature of the base class's member.

class MyBase {
    def method(self) {}
    def method(self, value) {}
}

class MyClass : MyBase {
    override def method(self) {}
    // error: member 'method' is marked override but is missing signature '()'(2) for class 'MyClass'
}

Base class members' default value is accessible from derived classes. To access a base class member, the base class name is used followed by the . operator and the member name. This behavior allows to use base classes' constructor/destructor, method implementation, and default values.

Important

To use a base class method implementation, the object instance must be explicitly passed as the first parameter.

class MyBase {
    def new(self) {}
    def method(self) {}

    ## member = false
}

class MyOtherBase {
    def new(self) {}
    def method(self) {}

    ## member = true
}

class MyClass : MyBase, MyOtherBase {
    def new(self) {
        if self = MyBase.new(self) {
            if self = MyOtherBase.new(self) {
                self.member = MyBase.member
                return self
            }
        }
    }

    def method(self) {
        return MyBase.method(self) + MyOtherBase.method(self)
    }

    - member = false
}

Note

Base class methods can also be redefined in the constructor of the derived class. The method override will then be on the instance and not on the whole class.

class MyBase {
    ## def method(self) {
        return false
    }
}

class MyClass : MyBase {
    def new(self, override) {
        if override {
            self.method = def (self) {
                return true
            }
        }
        return self
    }
}

Instantiation

A user-defined type instance is created with the call operator on the type. The operator gives an object containing a copy of each non-global member of the type initialized by the constructor if any.

The amount of parameters of the operator depends on the constructor, but the first parameter must not be passed. If the type does not have a constructor, the operator takes no parameters.

class MyClass {
    def new(self, value) {}
}

let myObject = MyClass(5)

Note

The instantiation of an enumeration is meaningless because those types can only have global members.

To access a member of the object, the . operator is used. If the member is a method and the call operator is used on the member, the object will automatically be passed as the first parameter.

class MyClass {
    def method(self, value) {}
    member = 5
}

let myObject = MyClass()
myObject.member    // gives 5
myObject.method(5) // call MyClass.method(myObject, 5)

The ?. operator can also be used to access a member of the object. With this operator, if the object is [[none|Built-in-types#none]], the object is returned; otherwise the behavior is the same as with the . operator.

let myObject = MyClass()
myObject?.member    // gives the value of myObject.member

myObject = none
myObject?.member    // gives none

Note

To call a member function without using the object instance as the first parameter, the ( and ) operators can be used around the function access.

(myObject.func)() // call the function with no parameters

Global members can be accessed in the same way, but they can also be directly accessed from the class.

class MyClass {
    @member = 5
}

myObject = MyClass()
myObject.member   // gives 5
MyClass.member    // gives 5

Important

The object instance is not passed to the parameter list when the called method is global.

A user-defined type can also be stored in a variable. This allows for the use of types as parameters or to create type aliases. To store the type, the type name can be used without the call operator.

@const TheClass = MyClass      // TheClass is an alias of MyClass
createList(MyClass, 5)   // Call createList with MyClass as the first parameter

Copy

If a copy of an object is required, a new instance will be created without member initialization or a call to the constructor. The value of each member will then be copied from the object to the copy instance recursively. This mechanism keeps track of any copied instance created for each value, and the same instance will be used if the same value is used more than once.

If extra operations should be performed during the copy, a special member clone can be declared. If this attribute is added to a class, the standard copy mechanism is disabled, and the method should be called instead. This member will also disable the standard copy mechanism for any class that uses the class using inheritance or composition.

class MyClass {
    // Count
    @copyCount = 0
    - value = 0

    const def clone(const self) {
        MyClass.copyCount++
        let other = self.allocate()
        other.value = self.value
        return other
    }
}

Important

The allocate method should be used to create the copy instance. Calling the class constructor could performs unwanted operations and break inheritance.

Tip

The clone member can also be declared as a private member to disable the copy.

Note

The mint.type module provides a clone function that calls the clone method of the provided object and falls back to the standard copy mechanism if not defined.

Internal types

A user-defined type like enum or class can be declared in a function body. These local types behave exactly like regular user-defined types but are only visible within the scope in which they are declared.

def func() {

    enum Foo {}

    class Bar {}

    defined Foo  // gives true
    defined Bar  // gives true
}

defined Foo      // gives false
defined Bar      // gives false

Local classes support all standard features, including fields, methods, and inheritance:

const def func() {

    class Base {
        const def name(self) {
            return 'Base'
        }
    }

    class Derived : Base {}

    let obj = Derived()
    typeof obj  // gives 'Derived'
}

Local classes defined within the same scope can reference each other, as well as themselves, from within their method bodies. Name resolution follows the lexical scope in which the classes are defined.

const def func() {

    class A {
        def makeB(self) {
            return B()
        }
    }

    class B {
        def name(self) {
            return 'B'
        }
    }

    let a = A()
    let b = a.makeB()

    b.name()  // gives 'B'
}

They can safely escape their defining scope by being returned or stored, as they do not rely on any external execution context.

To export a local type to the global scope, the type definition can be prefixed with an @:

def func() {

    @enum Foo {}

    @class Bar {}

    defined Foo  // gives true
    defined Bar  // gives true
}

defined Foo      // gives true
defined Bar      // gives true

« Function definition   Work with modules »
⚠️ **GitHub.com Fallback** ⚠️