Link Search Menu Expand Document

Classes

Table of contents

  1. Classes
  2. Defining a class
  3. Access Levels
  4. Constructor
    1. Implicit properties
  5. Methods
    1. toString
  6. This
  7. Attributes
    1. hasAttribute
    2. getAttribute
    3. setAttribute
    4. Optional Chaining
  8. Class variables
  9. Static methods
  10. Inheritance
  11. Abstract classes
  12. Traits
  13. Copying objects
    1. obj.copy()
    2. obj.deepCopy()
  14. Checking instance types
    1. instance.isInstance(class)
  15. Annotations

Classes

Classes provide a means to gather functions and data together to provide a blueprint for objects.

Defining a class

class SomeClass {
    // Constructor
    init() {
        print("Object created!");
    }
}

SomeClass(); // Object created!

Access Levels

A big part of the OOP paradigm is encapsulation, the fact that the outside interface only shows what it needs to show and things internal to the class do not need to be known to the user of the interface. Dictu manages this concept much the same as other languages through access levels. Unlike other languages, Dictu only has two access levels, public or private. If a method or instance variable is marked as private it is only accessible from within the class itself. To mark an instance variable as private it can be done in two ways, via implicit properties or by marking the property as private (note this must be done before defining the property otherwise it will be publically visible). To mark a method as private, preface the name with private.

class SomeClass {
    private x;
    
    init(x) {
        this.x = x;
    }

    private getX() {
        return this.x;
    }
}

print(SomeClass(10).getX()); // 10
print(SomeClass(10).x); // Cannot access private property 'x' on 'SomeClass' instance.

Constructor

init() is the method name for a constructor in Dictu. A constructor is a method that is called when an object is instantiated. Instantiating an object, is just like invoking a function, except you “invoke” the class. You can also pass arguments to the constructor to be used.

class SomeClass {
    // Constructor
    init(message) {
        print(message);
    }
}

SomeClass("Object created!"); // Object created!

Implicit properties

Dictu actually has a way to define properties on the object without explicitly setting each variable passed into the constructor on the object through this. If var is used the instance variable has public visibility, if private is used the instance variable has private visibility.

class SomeClass {
    // The var keyword here makes the argument passed in be set as an instance variable with public visibility
    init(var a, var b) {}
}

var obj = SomeClass(10, 20);
print("{} {}".format(obj.a, obj.b)); // "10 20"

The var or private keywords are optional on the constructor parameters, and can be in any order.

class SomeClass {
    init(var a, b, c, var d) {
        // b and c are not set as instance properties
    }
}

var obj = SomeClass(10, 20, 30, 40);
print("{} {} {} {}".format(
    obj.getAttribute("a"),
    obj.getAttribute("b"),
    obj.getAttribute("c"),
    obj.getAttribute("d")
)); // "10 nil nil 40"

Methods

Methods are functions defined within a class. When defining a method in Dictu, the def keyword is not used and instead its just the method name and parameter list.

class SomeClass {
    // Method
    printMessage() {
        print("Hello!");
    }
}

SomeClass().printMessage(); // Hello!

toString

Classes and instances can both be converted to a string using the toString method. If you want a different string representation for an object you can overload the toString method in your class.

class Test {}

class TestOverload {
    init() {
        this.name = "Testing";
    }

    toString() {
        return "{} object".format(this.name);
    }
}

print(Test.toString()); // '<cls Test>'
print(Test().toString()); // '<Test instance>'

print(TestOverload.toString()); // '<cls TestOverload>'
print(TestOverload().toString()); // 'Testing object'

This

this is a variable which is passed to all methods which are not marked as static. this is a reference to the object you are currently accessing. this allows you to modify instance variables of a particular object.

class SomeClass {
    // Constructor
    init(message) {
        this.message = message;
    }

    printMessage() {
        print(this.message);
    }
}

var myObject = SomeClass("Some text!");
myObject.printMessage(); // Some text!

Attributes

Attributes in Dictu are instance attributes, and these attributes get defined either inside the methods or on the object directly.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
print(myObject.x); // 10

hasAttribute

Attempting to access an attribute of an object that does not exist will throw a runtime error, and instead before accessing an attribute that may not be there, you should check if the object has the given attribute. This is done via hasAttribute.

Note: Will only check properties with public visibility.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
print(myObject.hasAttribute("x")); // true
print(myObject.hasAttribute("y")); // false

print(myObject.z); // Undefined property 'z'.

getAttribute

Sometimes in Dictu we may wish to access an attribute of an object without knowing the attribute until runtime. We can do this via the getAttribute method. This method takes a string and an optional default value and returns either the attribute value or the default value (if there is no attribute and no default value, nil is returned).

Note: Will only retrieve properties with public visibility.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
print(myObject.getAttribute("x")); // 10
print(myObject.getAttribute("x", 100)); // 10
print(myObject.getAttribute("y", 100)); // 100
print(myObject.getAttribute("y")); // nil

setAttribute

Similar concept to getAttribute however this allows us to set an attribute on an instance.

Note: Will set a property with public visibility.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
myObject.setAttribute("x", 100);
print(myObject.x); // 100

Optional Chaining

Optional chaining allows you to read a property or method of an instance without explicitly having to check for nil before attempting to access.

Note: If the left hand value is not nil the property / method must still exist when attempting to access otherwise a runtime error will occur.

class Test {
    // Lets say you have a method that 
    // could return an object or nil
    someMethod() {
        return nil;
    }
    
    someOtherMethod() {
        print("method!");
    }
}

// Here there is no explicit nil check.
print(Test().someMethod()?.someOtherMethod()); // nil

// If the operand is not nil the method / property must exist  
print(Test()?.unknownMethod()); // Undefined property 'unknownMethod'.

Class variables

A class variable, is a variable that is defined on the class and not the instance. This means that all instances of the class will have access to the class variable, and it is also shared across all instances.

class SomeClass {
    var classVariable = 10; // This will be shared among all "SomeClass" instances

    init() {
        this.x = 10; // "x" is set on the instance
    }
}

print(SomeClass.classVaraible); // 10

var x = SomeClass();
var y = SomeClass();

print(x.classVariable); // 10
print(y.classVariable); // 10

SomeClass.classVaraible = 100;

print(x.classVariable); // 100
print(y.classVariable); // 100

Static methods

Static methods are methods which do not reference an object, and instead belong to a class. If a method is marked as static, this is not passed to the object. This means static methods can be invoked without instantiating an object.

class SomeOtherClass {
    init(someArg) {
        this.someArg = someArg;
    }
    
    // 'this' is not passed to static methods
    static printHello() {
        print("Hello");
    }

    printMessage() {
        print("Some Text!");
    }
}

SomeOtherClass.printHello();
SomeOtherClass.printMessage();

Output

Hello
[line 17] in script: 'printMessage' is not static. Only static methods can be invoked directly from a class.

Inheritance

The syntax for class inheritance is as follows: class DerivedClass < BaseClass. super is a variable that is reference to the class that is being inherited.

Note: private methods and instance variables are not inherited.

class BaseClass {
    init() {
        this.someVariable = "Hello!";
    }

    printMessage(message) {
        print(message);
    }
}

class NewClass < BaseClass {
    init() {
        super.init();
    }
}

var obj = NewClass();
obj.printMessage("Hello!"); // Hello!
print(obj.someVariable); // Hello!

Abstract classes

An abstract class is a base class that can not be instantiated, like a trait, however is much like a contract in that it defines methods that need to be implemented within a class. An abstract class can have methods which implement the body, and would work like a normal class being inherited, however, if it includes methods which have been marked as abstract, it enforces the inheriting class to implement these methods.

abstract class AbstractClass {
    // We do not define the body of an abstract method
    abstract test()
    
    // We can also provide methods with the body that will be inherited as normal
    anotherFunc() {
        print("Func!");
    }
}

// If we left the class as is, a runtime error would occur.
// Class Test does not implement abstract method test
class Test < AbstractClass {}

class Test < AbstractClass {
    // We have implemented the abstract method, and therefore, satisfied the abstract class
    test() {
        print("Test!");
    }
}

Traits

Dictu only allows inheritance from a single parent class, which can cause complications when we need functionality from more than one class. This is where traits come into play. A trait is like a class, in the fact it has methods, and can deal with object attributes however, differ in the fact a trait can not be instantiated on its own.

trait MyTrait {
    hello() {
        print("Hello {}".format(this.name));
    }
}

class MyClass {
    use MyTrait;

    init(name) {
        this.name = name;
    }
}

var myObject = MyClass("Jason");
myObject.hello(); // Hello Jason

MyTrait(); // Runtime error: 'trait' is not callable

Sometimes we will have multiple traits, each with slightly different functionality, but we need functionality from all of these traits, in this instance, we can just use more than one trait.

trait MyTrait {
    hello() {
        print("Hello {}".format(this.name));
    }
}

trait MyOtherTrait {
    test() {
        print("Test!");
    }
}

class MyClass {
    use MyTrait, MyOtherTrait;

    init(name) {
        this.name = name;
    }
}

var myObject = MyClass("Jason");
myObject.hello(); // Hello Jason
myObject.test(); // Test!

Traits also do not suffer from the diamond problem unlike multiple inheritance, instead if two traits are used and they have the same method, the last most used trait has precedence. This means the order of trait inclusion into a class is important.

trait MyTrait {
    hello() {
        print("Hello {}".format(this.name));
    }
}

trait MyOtherTrait {
    hello() {
        print("This will not be ran!");
    }
}

class MyClass {
    use MyOtherTrait, MyTrait; // Order is important

    init(name) {
        this.name = name;
    }
}

var myObject = MyClass("Jason");
myObject.hello(); // Hello Jason

Copying objects

Class instances are a mutable type, this means if you were to take the reference of an object and use it in a new variable and you mutate the instance in the new variable, it would mutate the object at the old variable, since its a reference to the same object.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
var myNewObject = myObject;
myNewObject.x = 100;
print(myObject.x); // 100

To get around this, instances have two methods, obj.copy() and obj.deepCopy().

obj.copy()

This method will take a shallow copy of the object, and create a new copy of the instance. Mutable types are still references and will mutate on both new and old if changed. See obj.deepCopy() to avoid this.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
var myNewObject = myObject.copy();
myNewObject.x = 100;
print(myObject.x); // 10

myObject.obj = Test(); // Reference to a mutable datatype
myNewObject = myObject.copy();
myNewObject.obj.x = 100;
print(myObject.obj.x); // 100

obj.deepCopy()

This method will take a deep copy of the object, and create a new copy of the instance. The difference with deepCopy() is if the object contains references to any mutable datatypes these will also be copied and returned as new values meaning, they will not be mutated on the old object.

class Test {
    init() {
        this.x = 10;
    }
}

var myObject = Test();
var myNewObject = myObject.deepCopy();
myNewObject.x = 100;
print(myObject.x); // 10

myObject.obj = Test(); // Reference to a mutable datatype
myNewObject = myObject.deepCopy();
myNewObject.obj.x = 100;
print(myObject.obj.x); // 10

Checking instance types

instance.isInstance(class)

Checking if an instance is of a given class is made very simple with the isInstance method. This method takes in a class as an argument and returns a boolean based on whether or not the object was instantiated from the given class. Since classes can inherit other classes, and we know subclasses have the type of their parent class, the same holds true for isInstance(). If the instance being checked is passed it’s parent class as an argument isInstance() will evaluate to true.

class Test {}

var obj = Test();

print(obj.isInstance(Test)); // true

// Inheritance

class Test {}
class AnotherTest < Test {}

var testObj = Test();
var anotherTestObj = AnotherTest();

testObj.isInstance(AnotherTest); // false
anotherTestObj.isInstance(AnotherTest); // true
anotherTestObj.isInstance(Test); // true

Annotations

Annotations are metadata that are applied to classes that by themselves have no impact. They, however, can provide user defined changes at runtime to given classes.

@Annotation
class AnnotatedClass {

}

Annotations are accessed via the .annotations property available on all classes. If annotations are preset a dictionary is returned, otherwise the .annotations property is nil.

print(AnnotatedClass.annotations); // {"Annotation": nil}

Annotations can also be supplied a value, however, the value must be of type: nil, boolean, number or string.

@Annotation("Some extra value!")
class AnnotatedClass {

}

print(AnnotatedClass.annotations); // {"Annotation": "Some extra value!"}

Multiple annotations can be supplied to classes.

@Annotation
@AnotherAnnotation(10)
@SomeOtherAnnotation
class AnnotatedClass {

}

Note: Annotations are not available on methods.


This site uses Just The Docs, with modifications.