Objects are state machines that are instantiated on demand and released from memory when they are no longer needed. You can create global objects using "set", or local objects using "new" keyword.
Classes are objects themselves. Also, classes can be used to generate object instances. All object instances generated from a class, will inherite class structure and behaviour. The most important characteristics of objects are:
Class is a type declaration. You can declare the attributes of future objects before implementing the class. This is called "forward declaration". It is useful when you have 2 classes to be defined that depend on each other.
#define an object instance
** declare a class with two attributes
class MyClass = {a, b: Integer} <: Object;
** create several class signatures in a "class region"
class
Foo = {attr:Type, ...} <: Object;
Bar = {attr:Type, ...} <: Object;
...
Eve is similar to Java. It has a root Object that is actually a composite data type. This can be used as a base class. The Object constructor accept variable number of arguments. You can initialize an object by using the Object constructor.
#define an object instance
process test:
** initialize object using Object() constructor
new object_name := Object(attribute1:value1,
attribute2:value2,
...);
...
return;
Note: Object constructor can receive argument names that do not exist, and bind values to new attributes using (key:value) pairs. This is possible in Eve due to a gradual typing system.
Methods are related to an object or a class. The methods can have parameters and can return results. You must define all methods in the same module as the class declaration.
# define a "Point" class:
driver test_point:
class Point = {x, y :Double} <: Object;
method (@self:Point) move(a, b: Double):
let self.x += a;
let self.y += b;
return;
method (@self:Point) string() => (string:String):
let string := "# = (#:#)" ? (self.id, self.a, self.b);
return;
process
** initialize the points
new p := Point(x:1, y:2);
** move the point
p.move(2,1);
print p.string(); -- (3:3)
return;
Note: To call a method, you do not use any keyword. Just specify the method name with arguments. Methods can be created in packages to extend the Eve language with domain specific actions.
A class can define attributes for future object instances. The class constructor must set initial values for object attributes and can create new private attributes.
To access public attributes you must use dot notation like:
object_instance.public_attribute;
To access private attributes you must use "self." qualifier. Private attributes are available in constructor and inside of object methods but are not available with object name as qualifier. That is, private attributes are encapsulated.
self.private_attribute;
Note A class does not know anything about its objects. You can not ask the class anything about its instances, except if you create a special logic using class properties.
A class can have properties and methods. Class properties and metods are static members and must be defined inside the class declaration region.
An object can have attributes and methods. Attributes are defined by the constructor. Object methods are defined later in the same module, outside of the class body. Object attributes and methods are dynamic.
A class can have public or private properties. Each property is defined in the class context. We declare class properties after symbol ":" before any class method.
You can define methods inside of a class body. Class methods have access to all class members, without using any qualifier but have no access to object instance. @self object is not available in class methods:
Next example demonstrate a class that has no attributes and no constructor. This kind of class can't be instantiated. Is an abstract class, but it can be inherited by other classes.
driver test_method:
# define a class that has no constructor
class ClassName <: Object:
** class propetyes
set _sum: Integer; -- protected
set last: Integer; -- public
** class method (change state)
method change(param:Type):
let last := param;
let _sum += param;
return;
** create a getter for _sum
method sum() => (@result:Integer):
let result := _sum;
return;
return;
process
** access class method
ClassName.change(10);
ClassName.change(12);
** inspect ".last" property
expect ClassName.last == 12;
expect ClassName.sum() == 22;
return;
You can define methods in modules that are usable with any basic type similar to class methods. Also, you can define general methods. These methods are not bound to a particular data type.
module console:
** module method
method print (*args:Variant):
...
return;
** module initialization
init
...
return;
Note:Module methods will be useful, when we create the system library. Is enaugh to say we have left the design open, so that we can bootstrap Eve later.
Using the "Object" as the "root class", you can create a hierarchy of classes. Each classes is derived from the Object or from other "superclass" forming a "class tree". Like a real tree the class hierarchy has a single root.
Class Tree
You can create more complex objects by using a class constructor. This is a special struture that can be used to describe class members and object instances. A class can have a constructor or can be constructor-less.
A class is a composite data type. It can be derived from another data type or from the root Object. Next design pattern explain how to declare a class with attributes and constructor:
# user defined class
class NewType = {attributes} <: Object:
** declaration
...
create (parameters) => (@self :NewType):
** constructor
...
release
** cleanup
...
return;
Notes: A class has a declaration region where you can define static members, and an initialization region "create". This region is called contructor. A class can have one single constructor. The constructor is optional.
Constructors can have one or more parameters, some may be optonal. You can use a decision statements to initialize the object attributes in different ways based on parameter values.
# define a class
class ClassName = {attributes} <: Object:
** define class properties
set property_name := value;
....
** define class method
method test() => (result:Type):
...
let result := expression;
return;
create(parameters) => (@self: ClassName):
** conditional initialization
if condition then
let self.attribute = value;
...
else
let self.attribute = other_value;
...
done;
return;
A constructor can have parameters with an initial value that are optional. Parameters with no initial values are mandatory. It is possible to have a variable number of arguments, received as a HashMap for a vararg based constructor.
# define a class derived from root Object
class NewName += {attribute:Type, ....} <: Object;
** class body
...
create(*parameters: HashMap) => (@self:NewName):
...
return;
process
** instantiate a local object using constructor
new object := NewName(param:value,...);
...
return;
Note:The object in the previous example, has type: NewName. So you can use the class name as a data type. The data type defined by the class is dynamicly created by the compiler.
Object named "self" is the current object name. This is the result of the class constructor. It uses prefix "@" to show that it is a reference. The "self" parameter is used for declaring object methods. In Java this is an implicit parameter. In Eve you must declare @self explicitly.
This example defines a "Point" class with two parameters. The parameters have default values so they are optional when you create a point. Read the example and the notes to understand how to use a user defined class constructor.
# demonstrate a class
driver point_demo:
** define class Point, from root Object
class Point = {x, y :Double} <: Object:
** no members
create(x = 0, y = 0 :Double) => (@self: Point):
** constructor
let self.x := x;
let self.y := y;
return;
# using the class
process
** initialize the points
new p1 := Point(x:1, y:2);
new p2 := Point(x:2, y:2);
** use string patterns to print attributes
print "p1 = (x:#n, y:#n)" ? (p1.x,p1.y);
print "p2 = (x:#n, y:#n)" ? (p2.x,p2.y);
return;
p1 = {x:1, y:2} p2 = {x:2, y:2}
This example defines a class that can track its instances explicit. You can use static properties of a class to record all instances newly created. On release, you can remove the object instance @self from the item list.
# demonstrate a self tracking class
driver self_track:
** define class Point, from root Object
class Point = {x, y :Double} <: Object:
** tracking information
set instances: Integer;
set items: ()Point; -- list of points
** constructor receive 2 parameters
create(x = 0, y = 0 :Double) => (@self: Point):
** alter attribute values
let self.x := x;
let self.y := y;
** add object to list of items
let items += @self;
let instances += 1;
release(@self: Point):
** remove the object from list
let items -= @self;
let instances -= 1;
return;
process
** initialize the points
new p1 := Point(x:1, y:2);
new p2 := Point(x:2, y:2);
** verify how many point
expect Point.instances == 2;
** use item list to print all points
cycle:
new i := 0;
for p in Point.items loop
let i += 1;
print "p#n = (x:#n, y:#n)" ? (i, p.x,p.y);
repeat;
return;
p1 = {x:1, y:2} p2 = {x:2, y:2}
We can use two comparison operators with objects: "is" and "==". First operator "is" will compare the object location. If the objects have the same location they represent the same object. Second operator: "==" compare object type and object attributes or value. There are complementar operators "is not" and "!=". That can be used to create conditionals.
In next examples we use a primitive type: Integer, that is actually a class. So any Integer number is an Object instance. Object instances are references and can be compared.
#object comparison demo
driver object_compare:
set m = n = 1: Integer;
process
** equal values and types
expect m == n;
expect not (m != n);
** not the same location
expect not (m is n);
expect m is not n;
** alter one value
let n := 2;
** equal values and types
expect m != n;
expect not (m == n);
return;
Eve support inheritance and pilymorphism. You can specify a supertype using the symbol "<:" that is equivalent to the keyword: "extend" used in Java. Using this symbol makes a class that has all attributes and methods of the super-class or super-type.
# define a class with inheritance
class BaseType = {attributes} <: Object;
** object method
method (@self:BaseType) demo(param:Type, ...):
...
return;
** create a descendent of BaseType
class NewType += {new_attributes} <: BaseType:
** class members
...
create(parameters) =>(@self: NewType):
** call base class constructor
let self := super(parameters);
** create new attributes
new self.new_attribute := value;
...
return;
** overwriten method
method (@self:NewType) demo(parameters) => (result:Type):
...
return;
Generics are Eve system lybraries. You can create sub-types using a generic. Next generics are pre-defined: {Array, List, DataSet, HashMap}. We will describe these data types in next chapter.
Eve enable 4 generic types. If more generic types are required in the future we will modify the language. We will describe these generics as collection types. All these generics are "Iterable" Objects.
** making an array
class UserArray = []Type <: Array;
** making a list
class UserList = ()Type <: List;
** making a data set
class UserSet = {}Type <: DataSet;
** making a dictionary
class UserMap = {}(Type:Type) <: HashMap;
First we define the ElementType, then we define a collection UserList. After this we define a new instantes of type UserList and we create new elements in this list.
# Using a generic type to create a table
driver test_generic:
** making an element type
class ElementType = {a,b,c: Integer} <: DataSet;
** making a list of elements
class UserList = ()ElementType <: List;
process
** instantiate UserList object
new myList := UserList();
** enqueue one member
let myList += ElementType(1,2,3);
** append one member
let myList := myList + ElementType(7,8,9);
print myList -- ({1,2,3},{7,8,9})
return;
Using a data literal you can shortcut the design pattern. This is possible due to type inference. Eve create the types for you. After collection is created you can add new elements.
** define object using type inference
new myList := ({1,2,3},{7,8,9});
** using introspection to find the type
print Type(myList); -- List
print Type(myList[1]); -- DataSet
print Type(myList[1][1]); -- Integer
Using JSON you can create complex data structures. In next example we create a list of objects. Each object can have same structure or different structure. This is possible due to dynamic nature of Objects.
driver catalog:
class Person = {name:String, age:Integer} <: Object;
global
set myList: ()Person;
process
** define object using type inference
let myList := (
{name: "Elucian", age: 56},
{name: "Daniel" , age: 45}
);
** using introspection to find the type
expect Type(myList) == List;
expect Type(myList[1]) == Person;
expect Type(myList[1].name) == String;
expect Type(myList[1].age) == Integer;
return;
Note: In a collection you can use objects of the same type or descendents types of the declared element type. In this case if you derive a type Employe from Person, then you can add Emplyes to this list.
Read next: Functions