Eve Generics
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
Tas 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)andf(x: Integer, y = 1 :Integer)both acceptf(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
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