Classes and objects
Object-oriented programming
Object-oriented programming (OOP) is a programming paradigm that focuses on the use of "objects" as basic programming units. An object is an instance of a class, which is a template or mold for creating objects.
An object has two main parts: properties (also known as instance variables) and methods (also known as instance functions). Properties represent the characteristics of an object, while methods represent the actions an object can perform.
OOP is based on three fundamental principles: encapsulation, inheritance, and polymorphism. Encapsulation refers to hiding the implementation details of an object, allowing objects to interact with each other through a public interface. Inheritance allows a class to inherit the characteristics and behaviors of another class, while polymorphism allows an object to take different forms depending on the context.
In JARU, you can create classes using the keyword class followed by the class name. Within the class, you can define properties (variables) and methods (functions) that are specific to that class.
Class declaration
In JARU, classes are a fundamental tool for object-oriented programming. A class is a mold or template for creating objects that share specific characteristics and behaviors.
Class declaration in JARU starts with the keyword class followed by the class name:
class Person
// properties and methods go here
end
This creates a class called Person. Inside it you can declare properties and methods.
Only methods (def), private declarations and operators (operator) can go inside class. A property isn't declared there with var, which doesn't compile: it's created in the constructor with this, as shown next.
Property declaration
In JARU, properties are values associated with a class instance. These properties can be created at runtime and can be accessed and modified both from within the class (using the keyword this) and from outside the class with the syntax myObject.property.
The keyword this in JARU refers to the current instance of an object in a class. It's used to access the class's properties and methods from within the class itself.
Property declaration in JARU is done dynamically, meaning it's not necessary to declare them previously in the class. The init method is a common way to initialize properties when creating a new instance of a class.
class Person
def init(name)
this.name = name
end
end
var p1 = Person("John")
var p2 = Person("Mary")
print(p1.name) // shows John
print(p2.name) // shows Mary
Properties can also be created and modified from outside the class:
class Person
def init()
end
end
var john = Person()
john.alias = "Krillin" // Creates the "alias" property in the "john" object
print(john.alias)
Properties are associated with an object instance, not the class itself. This means that each time a new instance of a class is created, an independent copy of its properties is created associated with the instance.
Method declaration
Methods are functions defined within a class and have access to the class's properties. The syntax for declaring a method in JARU is the keyword def followed by the method name, parameters in parentheses, and must end with end.
class Person
def greet()
print("Hello!")
end
end
Methods can also have parameters and return values:
class Calculator
def add(a, b)
return a + b
end
end
Methods can access a class's properties using this:
class Person
def init()
this.name = ""
end
def greet()
print("Hello! " + this.name)
end
end
Inside a method, the instance's properties and its other methods are always named with this.. Without it, JARU looks for a variable or a function with that name, and the program stops with ERR_NAME_NOT_FOUND (Undefined variable 'add'.):
class Counter
def init()
this.n = 0
end
def add()
this.n = this.n + 1
end
def addTwice()
this.add() // with this.: calls the method
this.add()
end
end
var c = Counter()
c.addTwice()
println(c.n) // 2
Methods belong to instances: they can't be called on the class. Counter.add() gives ERR_TYPE_MISMATCH (This object type does not support method calls.).
Constructors
The constructor is a special method called init() that's used to initialize a class object at the time of its creation:
class ClassName
def init()
// initialization code
end
end
The constructor is called automatically when a new object of a class is created. The constructor name must always be init and must be inside the class definition.
class Person
def init()
this.name = "No name"
this.age = 0
end
end
It's also possible to pass parameters to the constructor:
class Person
def init(name, age)
this.name = name
this.age = age
end
end
Constructor overloading
In JARU, overloading of the init constructor method is allowed, which refers to the ability to have multiple versions of this method with different arguments:
class Person
def init(name, age)
this.name = name
this.age = age
end
def init(name)
this.name = name
this.age = 0
end
end
var p1 = Person("John", 25)
var p2 = Person("Mary")
The versions are told apart by their number of parameters: when the instance is created, JARU calls the one with as many parameters as arguments you pass.
- If there's none with that number, the error is
ERR_INVALID_ARG_NUMBER(No constructor found for class 'Person' that takes 3 arguments.). - With no arguments and no
init()without parameters, the instance is created anyway, without going through any constructor:Person()gives a person with nonameorage.
Only init can have several versions. If you declare two methods with the same name, the second replaces the first.
Class inheritance
Inheritance in object-oriented programming is a mechanism that allows a class (called child class or subclass) to inherit the properties and methods of another class (called parent class or superclass).
In JARU, the concept of inheritance is implemented using the colon ::
class Parent
def init()
this.property1 = "Value"
end
def method1()
print("Hello, I am method 1 of parent")
end
end
class Child : Parent
def method2()
print("Hello, I am method 2 of child")
end
end
In this example, the Child class inherits from the Parent class, so it has access to property1 and method1, and can also have its own properties and methods.
The child class gets all the parent's public methods, its constructors and operators too: if the child doesn't have an init with the number of arguments you pass, the parent's is used. To change what a method does, declare another one with the same name in the child.
super
super.method(...) calls the parent's version of a method, even if the child has changed it. It's mostly used in the child's constructor, so that the parent's constructor sets up its part: super.init(...).
class Animal
def init(name)
this.name = name
end
def speak()
return "..."
end
def introduce()
return this.name + " says " + this.speak()
end
end
class Dog : Animal
def init(name, breed)
super.init(name) // the parent's constructor sets the name
this.breed = breed
end
def speak()
return "woof"
end
def speakLikeAnimal()
return super.speak() // the parent's version
end
end
var d = Dog("Rex", "greyhound")
println(d.introduce()) // Rex says woof
println(d.speakLikeAnimal()) // ...
println(d.breed) // greyhound
super always takes a dot and a method name after it, and only in a class that inherits from another: in a class without a parent, the compiler gives the error Can't use 'super' in a class with no superclass.
Encapsulation
Encapsulation hides the details of a class: what's private is only used from its methods. In JARU, private properties are declared with private at the beginning of the class (several on the same line: private balance, pin), and private methods, with def private:
class Account
private balance
def init(initial)
this.balance = initial
end
def deposit(amount)
this.balance = this.balance + this.fee(amount)
end
def private fee(amount)
return amount - 1
end
def getBalance()
return this.balance
end
end
var a = Account(100)
a.deposit(11)
println(a.getBalance()) // 110
From outside the class:
- reading a private property gives
ERR_ATTRIBUTE_NOT_FOUND(Undefined property or method 'balance'.); - calling a private method gives
ERR_PERMISSION_DENIED(Cannot access private method 'fee' from class 'Account'.).
Inside the class's methods you can read and change the private properties of any instance of that class, not only those of this: other.balance. Private methods, on the other hand, are only called on this: other.fee(1) gives ERR_PERMISSION_DENIED, inside the class too. Private properties and methods don't show up in properties(), methods() or hasAttribute(), and copy() and clone() copy them too.
a.balance = 5, outside the class, doesn't give an error, but it doesn't change the private property: it creates another one, public, with the same name. After that, a.balance reads the public one, and the class's methods keep using the private one.
In JARU there's no protected, and what's private belongs only to its class: the methods of a child class can't reach the parent's private properties or methods (ERR_ATTRIBUTE_NOT_FOUND). What the child can do is call the parent's public methods that use them.
Polymorphism
Polymorphism is one of the pillars of object-oriented programming that allows an object of one class to be treated as if it were from another related class. This means that the same action can have different behaviors in different objects, depending on their class.
For example, imagine you have a class called Animal and two subclasses, Cat and Dog. The Animal class has a method called makeSound that is overridden in the Cat and Dog subclasses to emit a different sound. When objects of each subclass are created and makeSound is called on them, each object emits a different sound, but they can still be treated as if they were objects of the Animal class.
In summary, polymorphism allows using the same action or method with different objects, and depending on their class, a different action is performed. In the super example, introduce() is in Animal, but it calls this.speak(): with a Dog, the dog's speak() runs.
Operator overloading
A class can decide what operators do with its instances: a + b, -a, a == b... Each operator is declared inside the class with operator, the symbol and its operands, and returns the result. The EasyRender example, installed with the IDE, uses it for its vectors.
class Vector2
def init(x, y)
this.x = x
this.y = y
end
operator +(a, b)
return Vector2(a.x + b.x, a.y + b.y)
end
operator -(a)
return Vector2(-a.x, -a.y)
end
operator *(v, k)
return Vector2(v.x * k, v.y * k)
end
operator ==(a, b)
return a.x == b.x and a.y == b.y
end
def text()
return "(" + this.x.toString() + ", " + this.y.toString() + ")"
end
end
var a = Vector2(1, 2)
var b = Vector2(3, 4)
var total = a + b
println(total.text()) // (4, 6)
var opposite = -a
println(opposite.text()) // (-1, -2)
var twice = a * 2
println(twice.text()) // (2, 4)
println(a == Vector2(1, 2)) // true
These operators can be overloaded:
+,*and/, with two parameters. The/one also works fordiv.-: with two parameters it's the subtraction (a - b), and with one, the minus sign (-a). You can declare both.==,!=,<,<=,>and>=, with two parameters. Each one is declared separately:!=doesn't come from==.!, with one parameter: it works for!aand fornot a.|and&, with two parameters. EasyRender uses them for the dot product and the cross product.
% (mod), ^, ~, <<, >>, and, or and square brackets can't be overloaded.
- The operator receives both operands, the left one first. Don't use
thisinside: when both operands are instances, the operator of the right one's class is used, andthisis that one. - The instance has to be on the left of a number:
a * 2calls your operator, but2 * agivesERR_TYPE_MISMATCH(Unsupported operand types for operator '*'.). - Without their
operator, arithmetic operators giveERR_INVALID_OPERATION(Operator '+' is not defined for the right-hand operand's type.). Comparisons don't give an error:==and!=check whether it's the same instance, and<,<=,>and>=givefalsewith two different instances. - Declaring with
operatoran operator that isn't on the list, or with the wrong number of parameters, doesn't compile, and the error says why: “the operator 'mod' can't be overloaded”, “the operator '+' takes 2 parameters”.
Instance methods
Every instance has these methods, without declaring them:
| Method | What it does | Returns |
|---|---|---|
methods() | Lists the public methods of its class, inherited ones too. Constructors come out as init@N, with their number of parameters. | A list of strings |
properties() | Lists its public properties. | A list of strings |
hasAttribute(name) | Checks whether it has a public property or a public method with that name. Constructors ("init") and these methods don't count. | true or false |
setProperty(name, value) | Creates or changes the public property name. Useful when the name is in a variable. | nil |
remove(name) | Removes the public property name. If it doesn't exist, or if it's a method, it does nothing. | nil |
copy() | A shallow copy (see below). | A new instance |
clone() | A full copy (see below). | A new instance |
namehas to be a string; otherwise, the error isERR_TYPE_MISMATCH.- The lists from
methods()andproperties()come out in the order the VM keeps them in, which isn't necessarily the declaration order.
class Player
def init(name)
this.name = name
this.lives = 3
end
def greet()
println("Hello, ", this.name)
end
end
var p = Player("Ana")
println(p.properties()) // [lives, name]
println(p.hasAttribute("greet")) // true
var field = "level"
p.setProperty(field, 7)
println(p.level) // 7
p.remove("level")
println(p.hasAttribute("level")) // false
println of an instance writes the name of its class (Player instance). And two instances are equal with == only if they're the same one, unless the class declares operator ==.
If a class declares a method called remove, copy, clone, methods, properties, hasAttribute or setProperty, from outside the class the VM's one runs, not yours: inventory.remove("sword") would try to remove the sword property instead of calling your method. From inside (this.remove(...)) yours is called. Use other names, like take or duplicate.
Copying an instance: copy() and clone()
Assigning an instance to another variable doesn't copy it: both variables point to the same one. To get another one:
copy()makes a shallow copy: the copy has its own properties, but the lists, maps and instances they hold are shared with the original.clone()makes a full copy: it also duplicates the lists, maps, arrays, instances and structs inside.
class Backpack
def init()
this.items = ["sword"]
end
end
var original = Backpack()
var copied = original.copy()
var cloned = original.clone()
copied.items.append("shield") // it's the same list as the original's
cloned.items.append("bow") // the clone has its own list
println(original.items) // [sword, shield]
println(cloned.items) // [sword, bow]
clone() doesn't duplicate bitmaps, sprites or other module objects: the clone uses the same ones as the original, and if you move the clone's sprite, the original's moves too. To get another sprite, create it with Sprite.new(sprite) (see Sprite).
clone() with objects that point to each otherIf there's a cycle inside what you clone, like a child that keeps its parent in a property, clone() never finishes copying: the VM closes without any message, and try doesn't catch it. Only clone what has no cycles, or copy by hand what you need. copy() doesn't have this problem.
All the known issues of JARU, together, are in Known issues.