eturn; -- end of class declaration Eve Classses
Sage-Code Laboratory
index<--

Eve Objects

Eve is an object oriented language. We define objects and classes. A class is a user defined data type, that encapsulate data structure and methods. An object is an instance of a class. We use classes to create data structures and algorithms.

Table of Contents


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.

JSON literals

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.

Comparing objects

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.

Example:

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;
Notes: Objects can't be abstract but you can define an abstract class that can be extended by other classes or user defined data types. Eve implements all 4 OOP principles.

Class signature

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;
  ...

Features:

Eve language implement all 4 oop principles:

Notes:

Class are user defined data types.

Object type

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.

Class members

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.

Class Properties

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.

Class Methods

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.:

Example:

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;

Object Constructor

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.

Design:

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.

Object Methods

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.

Parameters

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;

@self object

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.

Object Attributes

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.

Example:

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;

Output:

p1 = {x:1, y:2}
p2 = {x:2, y:2}

Example:

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;

Output:

p1 = {x:1, y:2}
p2 = {x:2, y:2}

Inheritance

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.

Pattern:


** 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;

What are Partials?

Partials: In Eve a a thing that is not fully defined is called

  • partial
  • and is equivalent to "traits" or "abstract" or "interfaces" in other languages. We do not make a distinction between interfaces, traits and abstract classes or methods. Partials can be "abstract classes" or "prototype constructors" that have partial declarations of methods without implementation (just the signature). These methods need implementation in the inherited class.

    Abstract Constructor

    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;
    

    Traits in Eve

    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;
    

    Class Tree

    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

    Class Tree

    Generics in Eve

    Eve supports generics, allowing for the creation of flexible, reusable code structures that can work with different types while maintaining type safety. Generics are used in Eve system libraries. 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.

    Generic Syntax

    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:

    Optional Type

    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.

    Example: Using Optional with Generics

    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.

    Generic constructor

    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;
    

    Generics Use cases

    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; 
    

    List of DataSet

    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;
    

    Type Inference

    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 in Eve

    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.

    Note: Method chaining is an optional feature in Eve. Developers can choose whether to implement it based on their specific needs and coding style preferences.

    How Method Chaining Works

    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.

    Example of Method Chaining

    
    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;
      

    Benefits of Method Chaining

    • More concise code
    • Improved readability for sequences of operations
    • Fluent interface design

    Considerations

    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.

    Best Practice: Consider breaking very long chains into multiple statements or using intermediate variables for better readability and easier debugging.

    Implementing Method Chaining

    To implement method chaining in Eve, developers need to:

    1. Modify method signatures to return @self
    2. Return @self at the end of each chainable method
    3. Ensure that the method performs its operation before returning

    Remember, 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