eturn; -- end of class declaration
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.
Using JSON you can create complex data structures of objects. 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.
We can use two comparison operators with objects: "is" and "==". First operator "is" will compare the object memory address. If the objects have the same address they represent the same object. Second operator: "==" compare object type and object attributes. 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;
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.
Classes are objects themselves, singleton objects actually. Also, classes can be used to create object instances. All object instances created from a class, will inherite class structure and behaviour. The most important characteristics of objects are:
#define an object instance
** declare a class with two attributes
class MyClass = {a, b: Integer} <: Object;
...
Eve language implement all 4 oop principles:
Class are user defined data types.
Eve is similar to both Java and Python. It has a root Object that is actually a composite data type called Object. This can be used as a base class to be inherited. The default 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: Default object constructor can receive argument names like (attribute1, attribute2) that are not declared initially, these bind values to new attributes using (key:value) pairs. This is possible in Eve due to a gradual typing system. This is a minor feature in Eve.
A class can have properties and methods. Class properties and metods are static members and must be defined inside the class declaration. Some classes do not have a class declaration body. These classes can have properties defined in the signature between brackets {} but can't have methods.
A class can have properties. We declare class properties after symbol ":" before any class methods in the class body. Class properties are by default private. Public properties are created with "." prefix. Usually a class use private properties, getters and setters if necesary.
You can define methods inside of a class body. These are "class methods". 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 the class methods, unless explicit injected as input/output parameter.:
Next example demonstrate a class that has no attributes and no constructor. This kind of class can't be instantiated. Is a prototype class. The purpose of a prototype class is to create a singleton object.
driver test_method:
# define a class that has no constructor
class ClassName <: Object:
** class propetyes
set sum: Integer; -- private property
set .last: Integer; -- public property
** method (change state)
method .change(param:Type):
let last := param;
let sum += param;
return;
** getter method
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 create more complex objects by using a "constructor". In Eve we name things like they are used in specific programming paradigms. The constructor is a special subprogram that can be used to create a class instance also known as object. A class can be constructor-less in this case you can't create instances of this kind of class.
Next design pattern explain how to declare a class with attributes and constructor:
# user defined class
class NewType = {attributes} <: Object:
** declaration
...
release
** cleanup
...
return;
** constructor is a special subprogram
constructor NewType(parameters) => (@self :NewType):
...
return;
Notes: A class can have many constructors, overloaded. The constructor is optional and it looks like a method except it uses "constructor" keyword and the name of constructor is the same as the name of the class.
A class instance can have attributes and methods. Class instances are objects. Object members are encapsulated in the constructor body. Constructors are defined in the same module, outside of the class body. Object methods are in fact closures and constructor is in fact a higher order function that create @self object.
Constructors can have one or more parameters, some may be optonal. It can also have vararg parameters and input/output parameters like any routine. 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;
return;
** comstructor with default and vararg parameter, that is a list of integers
constructor ClassName(param1:=value1, *param2:()Integer) => (@self: ClassName):
** conditional initialization (flexible constructor)
if condition then
let self.attribute = value;
...
else
let self.attribute = other_value;
...
done;
return;
Object named "self" is the current object name. This is the result of the constructor. It uses prefix "@" to show that it is a reference. The "self". In Eve you must declare @self explicitly, it repesents a pointer to the new object.
A class can define attributes, with types in the class signature. The class constructor will set initial values for the object attributes just created, and can create additional attributes explicit in the constructor using "new" statement.
To access public attributes you must use dot notation:
object_instance.public_attribute;
To access private attributes you must use "self." qualifier available in constructor. Private attributes are available in constructor and methods but are not available with object name as qualifier. That is, private attributes are protected so that developer will not modify them by mistake from outside.
self.private_attribute;
Note A class body does not know anything about its objects or object attributes. You can not ask the class anything about its instances, except if you create a special logic using class properties. Also, class methods, do not receive @self parameter so there is no way to access by mistake an attribute in a class method.
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.
# define a "Point" class with multiple constructors:
driver test_point:
** declare a class "Point"
class Point = {x, y :Real} <: Object:
** class method to calculate distance between two points
method .distance(p1, p2: Point) => (@result: Real):
let dx := p2.x - p1.x;
let dy := p2.y - p1.y;
let result := sqrt(dx*dx + dy*dy);
return;
return;
** primary constructor
constructor Point(x = 0, y = 0 :Real) => (@self:Point):
let self.x := x;
let self.y := y;
** instance method to move the point
method move(a, b: Real):
let self.x += a;
let self.y += b;
return;
** instance method to convert point to string
method string() => (string:String):
let string := "(#:#)" ? (self.x, self.y);
return;
return;
** second constructor
constructor Point(other: Point) => (@self:Point):
** call primary constructor
let self = Point(other.x, other.y);
return;
process:
** use different constructors
new p1 := Point(1, 2); -- two-parameter constructor
new p2 := Point(); -- no-parameter constructor (uses defaults)
new p3 := Point(p1); -- copy constructor
** print results
print "p1: #" ? p1.string();
print "p2: #" ? p2.string();
print "p3: #" ? p3.string();
** demonstrate distance calculation
new distance := Point.distance(p1, p3);
print "Distance between p1 and p3: #" ? distance;
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 :Real} <: Object:
** tracking information (private class properties)
set instances: Integer;
set items: ()Point; -- list of points
release(@self: Point):
** remove the object from list
let items -= @self;
let instances -= 1;
return;
** constructor receive 2 parameters
constructor Point(x = 0, y = 0 :Real) => (@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;
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}
Eve support multiple inheritance and polymorphism. You can specify one or more supertypes after the symbol "<:" in a list. This is the "inheritance" symbol. It can define a new class that inherite all attributes and methods of the super-classes or bas-class.
** define a BaseType for inheritance without a superclass
class BaseType = {attribute};
** define an abstract class without attributes
class AbstractType <: Object:
** abstract method to be implemented
method demo(parameters) => (result:Type);
return;
** create a descendent of BaseType and AbstractType
class NewType += {new_attribute} <: (AbstractType, BaseType):
** class members
...
** implement abstract method
method demo(parameters) => (result:Type):
...
return;
return;
** define constructor for the new class
constructor NewType(param1, param2) =>(@self: NewType):
** initialize object Attributes
let attribute := param1; -- standard public attribute
let self.new_attribute := param2; -- dynamic private attribute
...
return;
Partials: In Eve a a thing that is not fully defined is called
A constructor that has only signature for some methods is partial also. Inherited classes need to implement a constructor similar, with all the required methods as they are designed in the prototype constructor. The prototype constructors can be called in the inherited constructor.
# Partial class
class Shape = {name: String} <: Object:
method area() => (@result: Real);
method perimeter() => (@result: Real);
return;
# Partial constructor for Shape
constructor Shape(name: String) => (@self: Shape):
let self.name := name;
method getName() => (@result: String):
let result := self.name;
return;
** Partial method signatures
method area() => (@result: Real);
method perimeter() => (@result: Real);
return;
# Concrete class
class Circle = {radius: Real} <: Shape;
# Concrete constructor implementing the partial constructor
constructor Circle(name: String, radius: Real) => (@self: Circle):
super(name); -- Call partial constructor to set up basic structure
let self.radius := radius; -- Set additional state
** Implement required methods
method area() => (@result: Real):
let result := pi * self.radius * self.radius;
return;
method perimeter() => (@result: Real):
let result := 2 * pi * self.radius;
return;
return;
# Usage
process:
new myCircle := Circle("Small Circle", 5);
print myCircle.getName(); -- Uses method from partial constructor
print myCircle.area(); -- Uses implemented method
return;
Some classes are not inherited from Object or any base-class. In these cases, the partial class can't be instantiated using the default Object constructor, and are designed to be used to add bihaviour to a class. This kind of entity is called "trait" in other languages. In Eve this is just a different kind of partial.
# Partial (similar to a trait or interface)
class Printable:
method print(); -- method signature
method toString() => (@result: String);
return;
# Partial (similar to an abstract class)
class Shape:
** abstract method that need implementation
method area(@self:Object) => (@result: Real);
return;
** partial constructor
constructor Shape => (@self:Shape):
** Partial method that use "self", as argument
method describe() => (@result: String):
let result := "This shape has an area of # square units." ? self.area();
return;
return;
# Concrete class is inheriting from partials
class Circle = {radius: Real} <: (Object,Shape,Printable):
method area(@self: Shape) => (@result: Real):
let result := pi * self.radius * self.radius;
return;
return;
# Partial super constructor (high order function)
constructor ShapeConstructor(name: String) => (@self: Shape):
** This partial constructor create one hidden state
new self.state := 0;
** This method signature need implementation otherwise the result is Null
method getName() => (@result: String);
return;
# Concrete constructor implementing the abstract constructor
constructor Circle(name: String, radius: Real) => (@self: Circle):
** Call partal super constructor
let Circle := ShapeConstructor(name);
let self.radius := radius;
** implement required method
method getName() => (@result: String):
let result := name;
return;
** implement required method
method print():
print self.toString();
return;
** implement required method
method toString() => (@result: String):
let result := "Circle '#' with radius #" ? (self.getName(), self.radius);
return;
return;
# Usage
process:
new myCircle := Circle("Small Circle", 5);
myCircle.print();
print myCircle.describe();
return;
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 in this picture. However Eve support multiple roots inspired from Banyan Tree in Florida.
Class Tree
The basic syntax for defining a generic class in Eve is as follows:
class GenericClass(:T) <: SupetType:
** Generic method
method genericMethod(value: T) => (@result: T):
** Implementation
return;
return;
In this syntax:
(:T)
is a type parameter. It can be any valid identifier.T
as a type within the class definition.new instance := GenericClass(:Integer)(constructor_arguments);
Eve provides an Optional type, which is a variant that can be either a value of a specific type or Null. This is particularly useful for handling nullable values in a type-safe manner.
Here's an example demonstrating the use of Optional with a generic class:
# Define an Optional type
class Optional(:T) = {T | Null} <: Variant;
# Define a generic processor class using Optional
class Processor(:T):
method process(value: Optional(:T)) => (@result: Optional(:T)):
if value is Null then
let result := Null;
else
** Type-specific processing
if type(value) is Integer then
let result := value + 1;
else if type(value) is String then
let result := value + " processed";
else
let result := value; -- Default case
done;
done;
return;
return;
# Usage example
driver test_optional_processor:
** Create processor instances
new intProcessor := Processor(:Integer);
new stringProcessor := Processor(:String);
process:
** Test with non-null values
new intResult := intProcessor.process(:Integer,5));
print intResult; -- Output: Optional(6)
new stringResult := stringProcessor.process(Optional(:String)("Hello"));
print stringResult; -- Output: Optional("Hello processed")
** Test with Null
new nullResult := intProcessor.process(Null);
print nullResult; -- Output: Null
return;
return;
This example demonstrates how to use generics with the Optional type to create flexible, type-safe code that can handle nullable values elegantly.
The syntax for the generic constructor follows the same pattern as the class. We use special notation (:T) to receive the type, then use the type inside constructor budy and and second parameter list. That is, a constructor receive two sets of parameters with similar syntax both.
# Generic Number class that can work with different integer types
class Number(:T) <: Object:
set value: T;
return;
# Generic constructor for Number
constructor Number(:T)(initialValue: T) => (@self: Number(:T)):
let self.value := initialValue;
# Method to get the value
method getValue() => (@result: T):
let result := self.value;
return;
# Method to set the value
method setValue(newValue: T):
let self.value := newValue;
return;
# Method to add to the value
method add(addend: T):
let self.value += addend;
return;
# Method to multiply the value
method multiply(factor: T):
let self.value *= factor;
return;
# Method to print the value
method print():
print "Number value: #" ? self.value;
return;
return;
# Usage example
driver test_generic_number:
process:
# Create a Number with i32
new num32 := Number(:i32)(42);
num32.print(); # Output: Number value: 42
# Create a Number with i64
new num64 := Number(:i64)(9223372036854775807); # Max value for i64
num64.print(); # Output: Number value: 9223372036854775807
# Use the methods
num32.add(10);
expect num32.getValue() == 52;
num64.multiply(2);
expect num64.getValue() == 18446744073709551614;
# Test with Integer (boxed i64)
new numInteger := Number(:Integer)(1000);
numInteger.print(); # Output: Number value: 1000
numInteger.add(500);
expect numInteger.getValue() == 1500;
return;
return;
Eve implement at least 4 generic types. If more generic types are required in the future we will modify the language to add fundamental generic libraries and improve the language over time. We will describe generics for collections next. All these generics are "Iterable" Objects where "Iterable" will be a partial.
** 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 constant literal you can shortcut the design and create more simpe code. This is possible due to type inference. Eve create the types for you. After a 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
Note:
There is more to left unexplained about classes. We need to enable operator kind of method to be able to extend the language. Also we need to define traits, abstract classes and method chaining. These things are very important.Method chaining is an advanced programming technique available in Eve that allows developers to call multiple methods on the same object in a single statement. This can lead to more concise and readable code, especially when performing a series of operations on an object.
In method chaining, each method in the chain returns the object instance (usually referred to as @self
in Eve) after performing its operation. This allows the next method in the chain to be called on the same object.
class Number(:T) <: Object:
set value: T;
return;
constructor Number(:T)(initialValue: T) => (@self: Number(:T)):
let self.value := initialValue;
method add(addend: T) => (@self: Number(:T)):
let self.value += addend;
return @self;
method multiply(factor: T) => (@self: Number(:T)):
let self.value *= factor;
return @self;
method print() => (@self: Number(:T)):
print "Number value: #" ? self.value;
return @self;
return;
# Usage
process:
new num := Number(:i32)(10);
num.add(5).multiply(2).print();
return;
While method chaining can lead to more elegant code, it's important to use it judiciously. Very long chains can sometimes become difficult to read or debug. Developers should balance the benefits of conciseness with overall code clarity.
To implement method chaining in Eve, developers need to:
@self
@self
at the end of each chainable methodRemember, implementing method chaining is optional in Eve. Developers can choose to use traditional method calls if they prefer, or mix chained and non-chained methods as appropriate for their code structure.
Read next: Collections