Eve Generics

A generic class has type parameters, so one class works for many types: Number(:i32), Number(:Real). Collections and the Optional type are generics too. Functions, procedures, methods and aspects can be generic as well, and a name can be overloaded with several signatures.

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, DataMap}. 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) <: SuperType is
    ** Generic method
    method genericMethod(value: T) => (@result: T) is
        ** Implementation
    return;
end GenericClass;

In this syntax:

  • (:T) is a type parameter. It can be any valid identifier.
  • You can use T as a type within the class definition.
  • When instantiating the class, you specify the actual type:
    new instance := GenericClass(:Integer)(constructor_arguments);

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. The built-in short form is the optional type T? (see Data Types).

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) is
    method process(value: Optional(:T)) => (@result: Optional(:T)) is
        if value is Null do
            let result := null;
        else
            ** Type-specific processing
            if type(value) is Integer do
                let result := value + 1;
            else if type(value) is String do
                let result := value + " processed";
            else
                let result := value;  ** Default case
            done;
        done;
    return;
end Processor;

# Usage example
driver test_optional_processor is
  ** Create processor instances
  new intProcessor := Processor(:Integer);
  new stringProcessor := Processor(:String);

  process main is
    ** Test with non-null values
    new intResult := intProcessor.process(Optional(: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;
end test_optional_processor;

This example demonstrates how to use generics with the Optional type to create flexible, type-safe code that can handle optional values.

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 the constructor body and in the parameter list. The type list (:T) belongs to the class. The constructor has no name; its parameters follow the type list in the call: new Number(:i32)(42), and its result is @self.


** generic Number class that can work with different integer types
class Number(:T) = {value: T} <: Object is
    ** generic constructor: the result is the new number
    constructor(initialValue: T) => (@self) is
        let self := Object();
        let self.value := initialValue;
    return;

    ** get the value
    public method getValue(@self) => (@result: T) is
        let result := self.value;
    return;

    ** set the value
    public method setValue(@self, newValue: T) is
        let self.value := newValue;
    return;

    ** add to the value
    public method add(@self, addend: T) is
        let self.value += addend;
    return;

    ** multiply the value
    public method multiply(@self, factor: T) is
        let self.value *= factor;
    return;

    ** print the value
    public method print(@self) is
        print "Number value: {self.value}";
    return;
end Number;

  ** Usage example
  driver test_generic_number is
      process main is
          ** 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)(4000000000);
          num64.print();  ** Output: Number value: 4000000000

          ** Use the methods
          num32.add(10);
          expect num32.getValue() == 52;

          num64.multiply(2);
          expect num64.getValue() == 8000000000;

          ** Test with Integer (boxed i64)
          new numInteger := Number(:Integer)(1000);
          numInteger.print();  ** Output: Number value: 1000
          numInteger.add(500);
          expect numInteger.getValue() == 1500;
      return;
  end test_generic_number;

Generic functions and procedures

A function or a procedure can have two parameter lists. The first list receives one or more data types, written (:T) like the type list of a class; the second list receives the values. One algorithm is written once and works for every type that fits. A type parameter can ask for a trait: (:T <: Comparable) accepts only types that can be compared.


driver generic_functions is
  ** the largest element of any list of comparable values
  function largest(:T <: Comparable)(items: ()T) => (@result: T) is
    let result := items[1];
    for x in items do
      let result := x if x.compare(result) > 0;
    done;
  return;

  ** a generic procedure: any type can be printed
  procedure show_all(:T)(label: String, items: ()T) is
    for x in items do
      print "{label}: {x}";
    done;
  return;

  process main is
    print largest((3, 9, 2));                 ** 9: T is inferred, Integer
    print largest(:String)(("b", "c", "a"));  ** c: T is written
    show_all("n", (1, 2));                    ** n: 1  n: 2
  return;
end generic_functions;
  • The type list is optional at the call: Eve infers the types from the arguments. Write it when you want to be explicit, or when the arguments do not tell the type: make(:Integer)();
  • A lambda has no type list. When the work depends on the type, write overloaded functions (below);
  • This is why Eve has no self-calling lambdas: (…)(…) after a name is a type list followed by the arguments (see Anonymous functions).

Generic methods

A method of a generic class uses the type of its class. It can also have its own type list, and so can a method of a plain class.


class Box(:T) = {item: T} <: Object is
  ** T comes from the class, U from the method
  public method tagged(:U)(@self, tag: U) => (@result: String) is
    let result := "{tag}: {self.item}";
  return;
end Box;

new b := Box(:Integer)(item: 5);
print b.tagged(1);                 ** 1: 5      U is inferred
print b.tagged(:String)("box");    ** box: 5    U is written

Generic aspects

The process main of an aspect can have a type list. The driver applies the aspect with or without the types:


** asp/largest.eve
exclusive aspect largest is
  process main(:T <: Comparable)(items: ()T) is
    new top := items[1];
    for x in items do
      let top := x if x.compare(top) > 0;
    done;
    print "largest: {top}";
  return;
end largest;

** in the driver
apply largest((4, 11, 6));                    ** largest: 11
apply largest(:String)(("pear", "apple"));    ** largest: pear

The process main of a driver has no type list: nothing calls it.

Overloading

Functions, procedures and methods can share a name when their signatures differ. The signature is the number of parameters and their types and, for a function, the type of its result. Overloading is the answer to work that differs by type, where a generic would need a different formula for each type anyway.


** the type of the parameter selects
function half(x: Integer) => (@result: Integer) is
  new h := x / 2 :Integer;
  let result := h;
return;

function half(x: Real) => (@result: Real) is
  let result := x / 2.0;
return;

** the number of parameters selects
function area(side: Integer) => (@result: Integer) is
  let result := side * side;
return;

function area(w, h: Integer) => (@result: Integer) is
  let result := w * h;
return;

** only the result differs: the expected type selects
function unit() => (@result: Integer) is
  let result := 1;
return;

function unit() => (@result: String) is
  let result := "one";
return;

print half(8), half(9.0);          ** 4,4.5
print area(3), area(2, 3);         ** 9,6
new n := unit() :Integer;          ** 1
new s := unit() :String;           ** one

The rules:

  • Eve selects the signature at compile time, never at run time;
  • The call must match exactly one signature. print unit(); is a compile error: nothing says whether the result is an Integer or a String. A default value can also make two signatures match: f(x: Integer) and f(x: Integer, y = 1 :Integer) both accept f(5);
  • Two declarations with the same signature are a compile error;
  • A function and a procedure never share a name;
  • Methods are the exception: a method without a result and a method with a result can share a name and the same parameters. They are different methods. A call as a statement runs the first, a call in an expression runs the second.

class Counter = {n: Integer} <: Object is
  public method step(@self) is                     ** called as a statement
    let self.n += 1;
  return;

  public method step(@self) => (@result: Integer) is   ** called in an expression
    let self.n += 1;
    let result := self.n;
  return;
end Counter;

new c := Counter(n: 0);
c.step();             ** the method without a result
print c.step();       ** the method with a result: 2
Version: generic functions, procedures, methods and aspects, and overloading, come with Version 0.4 (level 5 of the tests). Generic classes are in Version 0.3.

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) <: DataMap;

List of DataSet

First we define the ElementType, then we define a collection UserList. After this we define a new instance of type UserList and we create new elements in this list.


# Using a generic type to create a table
driver test_generic is

  ** making an element type
  class ElementType = {a,b,c: Integer} <: Object;

  ** making a list of elements
  class UserList = ()ElementType <: List;

  process main is
    ** instantiate UserList object
    new myList := UserList();

    ** enqueue one member
    let myList <+ ElementType(1,2,3);

    ** append one member
    let myList <+ ElementType(7,8,9);

    print myList;  ** ({1,2,3},{7,8,9})
  return;
end test_generic;

Type Inference

Using a constant literal you can shortcut the design and create simpler 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.


Read next: Partials