Data Types

The purpose of any programming language is to manipulate data. On the lowest level, data is basically 0 and 1 signals. Eve is a high level language. It has complex data types. Eve has a gradual-typing system that will be explained below.

Native Types

Native types are implemented by the hardware. We name native types using a single lowercase letter followed by the number of bits used: {8, 16, 32, 64}. In next table we show the convention of notation for these types.

Type Name Name Min Maximum
i Signed 8 64
u Unsigned 8 64
f Float 32 64

This will give us the native types used in Eve:

  • Unsigned integers: {u8, u16, u32, u64}
  • Signed integers: {i8, i16, i32, i64}
  • Single / Double precision floats: {f32, f64}

This list is complete: there is no i128 and no f16. Only the Eve core libraries use native types. The system library uses them to create Primitive Types and to communicate with other languages. Eve scripts use primitive types. A native type can be boxed using a primitive type. We will define next the primitive types.

Primitive Types

Primitive types are predefined in Eve core library. They have an equivalent native type. Primitive types are slower than equivalent native types, occupy more memory but they have more features. Primitive types are created using OOP so they are actually classes.

Class Name Description Minimum Maximum
Byte Unsigned on 8 bit (u8) 0 2^8-1
Short Signed on 16 bit (i16) -2^15 +2^15-1
Integer Signed on 64 bit -2^63 2^63-1
Natural Unsigned on 64 bit 0 2^64-1
Real Double-precision float on 64 bit (f64), IEEE-754 binary64 -1.8 × 10^308 1.8 × 10^308
Float Single-precision float on 32 bit (f32) -3.4 × 10^38 3.4 × 10^38
Decimal Exact base-10 number, for money and SQL NUMERIC: 12.50d variable variable
Huge Signed integer on 128 bit (i128), the same on every OS: -9000000000000000000z -2^127 2^127-1
Rational Fraction of two Integers, written 3\4, evaluated when needed (planned, version 2) variable variable
Rune Single Unicode code point (UTF-32), 4 bytes 0 0x10FFFF
Ordinal Enumeration of named values, unsigned on 16 bit (u16) 0 2^16-1
Time Time of the day (24h) 0 86,399,999 ms
Duration Period of time: an object with the fields year, days, hours, min, sec, ms. Stored as signed 64-bit milliseconds -2^63 ms 2^63-1 ms
Logic Is a Ordinal subtype having values: False = 0, True = 1 0 1

Primitive types can be converted in native types using .value() method. This method will return the native type equivalent and will perform "Unboxing" operation. Value can't return null.

Note: Native and primitive types are automatically initialized to zero value unless specified otherwise using a data literal. Runes have initial value nil, the empty rune ''. A zero value is a value: the object exists. null is different: it means that no object exists, nothing is allocated (see Null and Optional Types).

Zero values

Every type has a zero value. A declaration without a value, new a, b :Integer;, gives every name the zero value of its type.

TypeZero value
Byte, Short, Integer, Natural, Huge0
Real, Float0.0
Decimal0d
Runenil, the empty rune ''
LogicFalse
Ordinalits first element
Time, Duration0 milliseconds
String, Text"", the empty string
List()
Array[]; an array with a fixed capacity holds the zero value of its element type in every place
DataSet{}
DataMap{:}
Object, recordan object whose attributes hold their zero values

Numeric Types

In Eve we can have two categories of numbers:

Category Eve Types
Discrete Byte, Short, Integer, Natural, Huge, Ordinal, Rune
Continuous Float, Real, Decimal, Rational

Discrete numbers:

Discrete numbers have a range that can be calculated from the number of bits n of the equivalent native type: 8, 16, 32 or 64.

  • signed: min = -2^(n-1), max = +2^(n-1) - 1
  • unsigned: min = 0, max = 2^n - 1
type Chars Equivalent min max maximum number
Integer 20 i64 -2^63 2^63-1 ≤ 9,223,372,036,854,775,807
Natural 20 u64 0 2⁶⁴-1 ≤ 18,446,744,073,709,551,615

For conversion into characters:

  • The number of characters required for Integer numbers is 20. (19+sign)
  • For Real numbers, conversion into characters is controlled by #precision directive

Continuous Numbers

The type Real is represented using floating precision numbers.
Floating decimal numbers are most simply described by 3 Integers:

  • s: a sign (0 or 1)
  • c: a coefficient
  • n: an exponent

The numerical value of a finite number is −1ˢ × c × 2ⁿ Using this formula Eve define two floating point types.

Float is single-precision 32-bit IEEE 754:
Real is double-precision 64-bit IEEE 754:

type Digits Equivalent maximum number
Float 7 f32 ≤ 3.4 × 10^38
Real 16 f64 ≤ 1.8 × 10^308
Rational variable q64 variable (*)

Note A Rational is a fraction of two Integers, written with a backslash: 3\4 or 7\3. The evaluation of a Rational is postponed until a value is needed. Then it is computed with a specific precision, so precision and range are variable. Complex is reserved for a later version and is not defined in 0.1.

Numeric literals

Example Description
0 Integer zero
123 Integer number using symbols: {0,1,2,3,4,5,6,7,8,9}
1/2 Expression: a division returns a Real, here 0.5
3\4 Rational: two Integers separated by a backslash, evaluated when needed
0.5 Real number use symbols: {.,0,1,2,3,4,5,6,7,8,9}

Example:


#numeric literals demo
primary aspect numeric_literals is

  ** define global states
  set g1 = 0.0 :Real;
  set g2 = 0.0 :Float;

  ** define main process
  process main is
    ** check equality
    expect g1 == g2;     ** value is equivalent
    expect g1 is not g2; ** not the same
    expect g1 not eq g2; ** different types

    ** define local variables
    new i :Integer; ** Initial value = 0
    new n :Natural; ** Initial value = 0
    new r :Real;  ** Initial value = 0.00
    new q = 3\4 :Rational; ** evaluated when needed

    ** use modifier := to change value
    let i := 9223372036854775807;  **  maximum
    let n := 18446744073709551615; **  maximum
    let r := 1/2; **  0.5
  return;
end numeric_literals;

See also: scientific notation

Type "Rune"

A Rune is a single Unicode code point, encoded on 32 bits (UTF-32). A single quoted literal holds one code point, in a fixed size of 4 bytes, unlike C, where a char has 1 byte. The name comes from the Go language, which calls this type "rune" too.

A Rune literal holds exactly one code point. 'a' and 'β' are Runes, but 'one' is a syntax error ("a symbol literal holds exactly one character"): text is written with double quotes, "one".

Examples


process main is
  ** several examples of local Rune type variables. 
  new letter   := 'a';
  new capital  := 'B';
  new number   := '5';
  new unicode  := 'β';
  new operator := '¬';
  ...
return;

As you can see in the example Rune literal is enclosed in single quotes '' follow by ";" to end the declaration statement. In Eve a Rune can hold 4 bytes but only one ASCII symbol or UNICODE symbol. The empty rune '' is valid and is named nil. null is not a rune: it means that no object exists.

Unicode Literal

Unicode literals start with prefix U+. Such literals are compatible with single quoted Rune code point. Unicode literals are case insensitive hexadecimals and do not require single quotes. There is one form only:

  • U+HHHH to U+HHHHHH: 4 to 6 hexadecimal digits, from U+0000 to U+10FFFF (the end of the Unicode range).

Examples


process main is
  ** Several examples of local Rune type variables. 

  new letter   := U+0061; ** Unicode for lowercase 'a'
  new capital  := U+0042; ** Unicode for uppercase 'B'
  new number   := U+0035; ** Unicode for digit '5'
  new unicode  := U+03B2; ** Unicode for beta (β) character
  new operator := U+00AC; ** Unicode for NOT operator (¬)

  ...
return;

Boxing and Unboxing

In Eve, "boxing" and "unboxing" are crucial concepts that describe the conversion between native types and their corresponding primitive types.

Boxing

Boxing is the process of wrapping a native type value within a primitive type object. This allows native types to be treated as objects, giving them access to methods and properties defined in the primitive type classes.


** Boxing (often implicit)
new x = 5 :Integer;  ** 5 (native i64) is boxed into an Integer object

Unboxing

Unboxing is the reverse process of extracting the native type value from a primitive type object. This is typically done when you need to perform low-level operations or interface with system libraries that expect native types.


** Unboxing (explicit)
new native_value = x.value() :i64;  ** Extracts the i64 value (core library code)

Key Points

  • Boxing is often implicit in Eve, happening automatically when you use a native value where a primitive type is expected.
  • Unboxing is typically explicit, using methods like .value() to retrieve the native value.
  • Primitive types in Eve are implemented as classes, providing additional functionality over their native counterparts.
  • Boxing allows for null values and additional methods, while native types are more memory-efficient but lack these features.
  • The use of boxing and unboxing allows Eve to balance performance with the benefits of an object-oriented type system.
Note: While boxing provides additional features, it comes with a small performance overhead. Use native types for performance-critical operations where the additional features of primitive types are not needed.

Example


# Boxing and Unboxing in action
primary aspect boxing_demo is

  process main is
    ** Boxing
    new boxed_integer = 42 :Integer;  ** 42 is boxed into an Integer object
    
    ** Using a method available on the boxed value
    print boxed_integer.to_binary();  ** Prints binary representation
    
    ** Unboxing
    new raw_value = boxed_integer.value() :i64;
    
    ** raw_value is now a native i64, without object methods
    print raw_value;  ** Prints 42
    
    ** This would cause an error:
    print raw_value.to_binary();  ** Error: i64 has no method 'to_binary'
  return;
end boxing_demo;

Understanding boxing and unboxing is essential for efficient memory management and performance optimization in Eve programs, especially when working with large datasets or in performance-critical sections of code.

Composite Types

Composite data types, also called structured types or user-defined types, group multiple primitive data types (like integers, strings) into a single unit. They define a structure to hold this data and often include ways to access and manipulate it.

Note: We define implicit data types for specific data literals. For most cases, implicit data type works correctly. Sometimes, implicit data type can create undesired data type. This is because data literals may overlap between different data types. To avoid this case you should use a type hint.

Class Name Description
Object Root class for all composite data types
Record Flat collection of native data types. (No objects)
Variant A union of possible data types.
Range Discrete range of numbers equally distanced (x..y)(ratio)
Pair A pair is like a list but it has only two elements (k:v)
String Double quote delimited sequence of symbols: "..."
Date Calendar date: object with the fields era, year, month, day.
List Dynamic ordered enumeration of values of same type
DataMap Enumeration of (key:value) pairs unique sorted by key; values may nest. A literal with quoted keys, {"key": "value"}, is a DataMap: this is how Eve reads JSON objects
DataSet Enumeration of unique elements of the same type
Table A collection of records indexed by unique key.
Function Function object type/ reference to function.
Vector Single-dimensional, fixed size array
Matrix Multi-dimensional , fixed size array
Error Composite type derived from Object base class that signify an exception.
Null The type of a missing object. Its only value is null: no object exists, nothing is allocated. See Null and Optional Types.

Range

A range is a notation that describe a set of discrete elements. Range is not expanded in memory. Ranges enable creation of efficient and comprehensive statements in a declarative way.

Usecases:

Ranges can be used in collection builders, make logic evaluations, or generate values for loops. Range logic operations should be optimized by the compiler. In for loops, elements will be generated one by one as needed until the loop end.

Syntax:

In next example we create a range class. The new range is actually a sub-class of Range. We will learn later how to use ranges in different cases. We use .. for the limits and a second pair of parentheses for the ratio (step).

class RangeName = (min..max)(ratio) <: Range;

Notes:

  • Limits are included by default,
  • Ratio is default 1 and optional,
  • Limits can be inverted,
  • Ratio can be negative,
  • Compiler should verify a range declaration to make sense,
  • The notation [x..y] is a slice, not a range: a slice uses a range as index, base[x..y].

Example:

Numeric ranges:


#numeric range demo
primary aspect numeric_range is

  ** define a validation type
  class R5 = (0..5) <: Range;

  process main is
    ** test range limits
    expect 0 in R5; ** assert logic expression
    expect 5 in R5; ** assert logic expression

    ** test limits with range literals
    expect 4 in (1..8)(2);
    expect 5 not in (-10..10)(2);
  return;
end numeric_range;

Example:

Rune range, specify Unicode symbols as limits.


# using symbolic ranges to print out a collection that do not exist
primary aspect symbol_range is
  process main is
    ** elements of these ranges are written one by one
    print ('0'..'5');
   ** ('0','1','2','3','4','5')
    print ('a'..'f'); ** ('a','b','c','d','e','f')
    print ('A'..'F'); ** ('A','B','C','D','E','F')

    ** use range as validation domains
    expect '0' in ('0'..'9');  ** range of digits
    expect 'c' in ('a'..'z');  ** range of symbols
    expect 'X' in ('A'..'Z');  ** range of symbols
  return;
end symbol_range;

Decimal range

It is possible to create a decimal range. For this, the limits must be decimal or at least the ratio must be decimal . If ratio is not specified, it is automatically created: 0.1 or 0.01 depending on precision of first limit. 0.0 or 0.00. In Eve the number of zeros can establish the precision for numeric ranges. The range literal indicates the precision: (0.0..1.0) and (0.00..1.00) are different ranges, the trailing zero is relevant.


# using fractional ranges
primary aspect fraction_range is
  process main is
    ** elements of these ranges are written one by one
    print (0..0.5)(0.1); ** (0.0,0.1,0.2,0.3,0.4,0.5)

    ** use range as validation domain
    expect 0.01  in (0.0..1.0); 
    expect 0.001 in (0.00..1.00); 
    expect 0.001 not in (0.0..1.0);   
  return;
end fraction_range;

Random Method

A range is iterable and has a number of methods that are useful for data processing, but it is not a collection: it is a generator. One method is very useful and is going to be used in many examples: random(). It is implemented for all collections.

Examples:

# range demo
primary aspect random_demo is

  class Small = (0..1)(0.1) <: Range;
  class Large = (0..2^64 ) <: Range;

  process main is
    ** you can call random() like this
    print Small.random(); ** 0.3
    print Large.random(); ** 65535

    ** you can call random() like this
    print random((1..2^32)(2));       ** 143453566436
    print random((1..2^32)(0.25));    ** 43.75
  return;
end random_demo;

Data Coercion

In computer science coercion is used to implicitly or explicitly change an entity of one data type into another of different type. This is ready to take advantage of type hierarchies and type representations. If not designed properly the coercion can be a fatal mistake. Eve is a safe language so we do only safe coercion.

Implicit coercion In Eve the arithmetic operators are polymorphic. Numeric operators can do implicit data conversion to accommodate the data types and return an accurate result. Automatic conversion is possible only when there is no risk of losing data precision. If there is a loss of precision we can end-up with a run-time error. To prevent this Eve will implement a safe compile-time check.

Notes:

  • Implicit conversion is possible and safe.
  • Explicit conversion is possible but unsafe.
#example of implicit conversion
primary aspect implicit_coercion is

  process main is
    ** local variables
    new a := 2;
    new b := 1.5;

    ** alter a, b
    let b := a;       **  this implicit cast is possible b = 2.0
    let b := a + 3.5; **  add 3.5 then assign result to b = 5.5
    let a := b;       **  error: can not assign Real to  Integer
    let a := 1.5;     **  error: can not assign Real to  Integer
  return;
end implicit_coercion;

Division The operator / always returns a Real, even when both operands are Integer: 4 / 2 is 2.0. To get an Integer, enforce the type in the declaration. The result is converted to Integer and the decimals are lost:

# division returns Real
primary aspect division_demo is

  process main is
    new a := 7;
    new b := 2;
    new r := a / b;          ** r is Real: 3.5
    new x = a / b :Integer;  ** x is Integer, the decimals are lost
  return;
end division_demo;

Explicit coercion Explicit coercion is a forced conversion. Can be used to convert backwards from higher data range to lower data range or from continuous numbers to discrete numbers. This however can cause a data or precision loss. Explicit coercion is using a function.

Examples of explicit coercion:

# explicit coercion in EVE
primary aspect explicit_coercion is
  set a = 0   :Integer;
  set b = 1.5 :Real;
  process main is
  **explicit coercion lose (0.5)
    let a := floor(b);
    write  a; ** will print: 1

  **explicit coercion add (0.5)
    let a := ceiling(b);
    print  a; ** will print: 2

  **explicit coercion rounding:
    let a := round(b);
    print  a; ** will print: 2
  return;
end explicit_coercion;

Decimal and Real A Decimal is exact and a Real is not, so the two are never mixed by an operator: 1.5d + 2.5 is a compile error. You convert one of them, with a method or with :=:

# Decimal and Real
primary aspect decimal_real is
  process main is
    new price := 12.50d;
    print price.real() + 0.25;     ** 12.75: d.real() gives a Real, it may lose digits
    print 0.1.decimal() + 0.20d;   ** 0.30: r.decimal() gives the Decimal of the shortest text, 0.1d
    new r := 0.0;
    let r := price;                ** := converts: r holds a Real, so it gets 12.5
    new x := 3.75d :Real;          ** a type hint converts too
  return;
end decimal_real;

A division of two Decimal values keeps the larger scale plus 6 digits: 1d / 3d is 0.333333, 10.00d / 4d is 2.50000000.

Number to a string

#convert number to string
primary aspect number_to_string is
  ** local states
  set s :String;
  set v :Integer;
  process main is
    let v := 1000;
    let s := format(v); **  explicit coercion s = "1000"
    expect (s == "1000");
  return;
end number_to_string;

String to a number

This can be read using the casting function parse(), only if the string contains a number. Otherwise the conversion fails and raises an exception. The type of the target decides the result: parse() coerces the value to the target type. An Integer target loses the decimals and a Real target preserves them. This is not an error: the developer asked for an Integer. In debug mode the compiler reports a warning "possible data loss" with the line number.

#string to number conversion
primary aspect string_to_number is

  ** global states
  set v :Integer  ;
  set b :Real   ;

  set s = "1000"  :String;
  set r = "200.02":String;
  process main is
    let v := parse(s); **  make v = 1000
    let v := parse(r); **  make v = 200 and decimal .02 is lost
    let b := parse(r); **  make b = 200.02 and decimal .02 is preserved
  return;
end string_to_number;

Note: Built-in functions that are located in EVE default library: { parse(), format(), ceiling(), floor(), round()}. This module is one of the standard modules that are automatically included in any Eve program.

Default Types

Literals are representations of specific particular data type in source code. We connect data literals to default types for every literal to be able to use type inference in declarations and expressions. In next table we show this association.

Note Next notation use "9" to show any digit in range (0..9) but it can be any digit. We also use A..F to show hexadecimal numbers examples. It can be of course any digit or letter in hexadecimal range.

Base-10 literals without a point are Integer, even when they are not negative. Hexadecimal and binary literals are bit patterns, therefore Natural.

Number suffixes. A letter after a base-10 literal gives its type. A hexadecimal literal takes no suffix, because b, d and f are hexadecimal digits (0x1d is 29): use a type hint, new mask = 0xFF :Byte;.

SuffixTypeExample
noneInteger, or Real when the literal has a point42, 3.14
dDecimal12.50d
rReal (double precision)5r
fFloat (single precision)1.5f
bByte255b; 0b is the Byte zero, 0b0 the binary literal zero
wShort (word)11w
nNatural42n
zHuge-9000000000000000000z
Literal Type
9 Integer
-9 Integer
0x9ABCDEF Natural
0b1010101 Natural
9.9 Real
U+0061 Rune
U+10FFFF Rune
'a' Rune
"a" String
1/2 Real (division)
3\4 Rational

Zero literals

Literal Type
[] Vector (empty)
{} DataSet (empty)
() List (empty)
"" String (empty)
'' Rune (nil, the empty rune)
0 Integer
0.0 Real

Collection literals

Literal Type
(1, 2, 3) List[Integer]
("1","2","3") List[String]
{a:0, b, c} Ordinal
{a:"x",y:2} Object
(1..5) Range
(k:v) Pair
{3, 4, 6} DataSet[Integer]
[1, 2, 3] Vector[Integer](3)
['a','b','c'] Vector[Rune](3)
["a","b","c"] Vector[String](3)
[[1,2],[2,4]] Matrix[Integer](2,2)

Type Inference

Type inference is a logical association of data type from a constant literal or expression. Type inference can be used in declarations of new variables with the specific assign operators capable to infer data types.

Type inference is enabled by operators {:=, ::}. You can use type inference with keyword "new" but not with keyword "set". Type inference can improve a type declaration if the declaration is partial using operator ":=".

Partial Inference:

In next example we use a List data literal to create a shared collection of type List[Integer]. However the type used is partial. Do not have element type, that is going to be Integer.

#test partial inference
primary aspect type_inference is
  ** Define a list without element type
  set ls :List;  ** partial type declaration

  process main is
    ** establish element data type
    let ls := (0,1,2,3,4,5,6,7,8,9); ** gradual typing
    print ls.type();
    expect type(ls[1]) is Integer;
  return;
end type_inference;

Collection Inference

You can declare empty collections using partial inference and determine the type of elements later, with an assignment. After type is established, it can't be changed later and start functioning as a contract. That is, you must respect the element types for next expressions.

# gradual type declaration
process main is
  new a := ();  ** empty List
  new v := [];  ** empty Vector
  new s := {};  ** empty DataSet

  ** add elements using gradual typing
  let a := (10,11,12);
 ** establish element type integer
  let a <+ "4";
        ** expect error, you can't add a string now
return;

Type Verification

We can verify the type using the "is" operator: x is Integer. The type of a value is also returned by type(), which exists both as a function, type(x), and as a method, x.type().

# using operator "is" to check type
primary aspect type_check is
  ** define object and initialize
  set r = {name:"test", age:24} :Object;
  
  ** define hash table
  set t = {"key1": "value1", "key2": "value2"} :DataMap;
  process main is
    ** check variable types using introspection
    expect type(r.name)  is String;
    expect type(r.age)   is Integer;

    expect type(t['key1']) is String;
    expect t is DataMap;
  return;
end type_check;

Printing type()

For type introspection we can use type() built-in function:

# introspection demo
primary aspect print_type is

  process main is
    new i := 1.5;
    expect type(i) is Real;
    new t := type(i);
    print "type of i is {t}";
  return;
end print_type;

String interpolation inserts the value of a name in a string with a placeholder, {name}, for example "param1: {a}"; after the format operator % you can give a format, for example "{a % i5}". A placeholder holds a name, never an expression or a call; a literal brace is \{ or \} (see Strings).

Polymorphic Operators

In mathematics there are very few operators: {+, -, ÷ , * } that can operate with any kind of numbers: negative, positive, rational or real. Operators are not bound to specific data types. Therefore these operators are called "polymorphic".

Implementation note: a compiler can implement operators as functions and overload the function signature using type dispatch, with the left side operand first (the leading operand; for unary operators the only operand). This is an implementation technique, not a rule of the language.

Logical Type

In Latin the "falsus" and "verum" are translated in English to "false" and "true". In many languages False and True are two constants. In Fortran we use to have .T. and .F. while in C we use to have {0, 1}. Two values.

  • falsus := ⊥ defined by ⊥ := ¬ ⊤ and ( P ∧ ¬P )
  • verum := ⊤ defined by ⊤ := ¬ ⊥ and ( P ∨ ¬P )
name value binary
False Logic.False 00000000 00000000
True Logic.True 00000000 00000001

Example:


process main is
  ** True and False are constants of type Logic, not Byte
  new on  = True;    ** on is Logic 
  new off = False;   ** off is Logic   

Internal design

In Eve we Logic type as a subtype of Ordinal:


class Logic = {False:0 , True} <: Ordinal;

So in Eve we have False.value == 0 and True.value == 1. These are Logic values not Boolean as in other languages. You can use False and True with == or <> to establish conditionals.

Logical expressions

A logical expression is a demonstration or logical deduction having result True or False. Operator precedence is: {not, and, or, xor}. The order of operations can be controlled using operator precedence and round parentheses. Relation operators {>, <, >=, <=, ==, <>} have higher precedence than logic operators.

Result of logical expressions can be used into a conditional statement to make a decision. Also results of logical expressions can be stored in logical variables to be used later in other conditions.

Gradual Typing

Gradual typing is a type system in which some variables may be given a type. Some variables or members may be left un-typed. Therefore some type errors are reported at compile-time some other at run-time.

Gradual typing is different than type inference. On type inference the type specification is missing but default type is determined by logic deduction from literals and expressions.

Variant Types

A Variant is a polymorphic variable that can have one of a list of types but only one at a time. When the value is established, one type to hold that value is created.

Syntax:

** define variant subtype
class VName = {Type1 | Type2 | ... } <: Variant;

**  declare variable (with initial value)
process main is
  new v = value :VName;

Properties

  • Variant data type is assigned at runtime;
  • One variant can have a single value at a time;
  • One variant is a "union" of several types;
  • Only "new" creates a variant. "set" makes constants and a constant has one clear type, never a variant;
  • Once a value is assigned, we know the variant has become one of its types.

Making a null-able type

A variant can include the type Null, so it may also hold null. The short form of this is an optional type, see Null and Optional Types.

Example:


** define nullable variant
primary aspect test_nullable is

  class Number = { Integer | Real | Null } <: Variant;

  process main is
    ** use nullable variant
    new x :Number;
    let x := 10;    ** x is Integer
    let x := 45.5;  ** x is Real
    let x := null;  ** no object
    expect x is Null;
    expect x == null;
  return;
end test_nullable;

Null and Optional Types

Null is a type and null is its only value. Names are case sensitive: Null (the type) and null (the value) are different names. null means that no object exists: nothing is allocated. o is Null tests the type and o == null the value. print writes the value, null; type(o) gives the type, Null.

  • A zero value is not null: 0, "", '' (nil), (), {} and [] are values, the object exists;
  • A variable declared without a value gets the zero value of its type, never null: new s: String; is "";
  • Only an optional type may hold null. It is written with ? after the type: Integer?, String?, Person?. A plain Integer never holds null;
  • An optional variable declared without a value starts as null.

Example:


# optional values
primary aspect test_optional is
  process main is
    new age: Integer?;          ** optional: starts as null
    new count: Integer;         ** plain: starts as 0
    expect age == null;
    expect count == 0;

    let age := 42;              ** now it holds a value
    expect age is Integer;

    let age := null;            ** and can lose it again
    expect age is Null;
  return;
end test_optional;

Usability

A variant can establish its data type at runtime:

Example 1:

In next example we use a variant that can be Real or Integer.

#variant type demo
primary aspect variant_type is

  process main is
    ** define variants: only "new" can create a variant
    new v, x, t :{ Real | Integer };
    ** using a variant first time 
    new t := 12;      ** assign an Integer (possible)
    print type(t);     ** Integer


    ** using a variant second time
    let t := 1 / 2;   ** assign a Real (possible)
    print type(t);     ** Real


    ** unsafe conversion
    new x := 1.5;  **  x is Real
    let v := 1;    **  v is Integer
    let v := x;    **  v becomes Real
    print type(v);  **  Real

  return;
end variant_type;

Example 2:

A variant is a way to circumvent the type system. It can be used in a generic method. For this we use variant parameters. Using variant parameters enable a method to be called with different argument types making the program more dynamic:

# variant parameter in methods
primary aspect variant_params is

  ** define a method that can swap two numbers
  ** requires input/output parameters marked with @
  procedure swap(@x, @y: {Integer | Real} ) is
    ** check type to be the same
    expect type(x) == type(y);

    ** swap x, y values

    new i := x; **  intermediate
    let x := y; **  first  swap
    let y := i; **  second swap
  return;

  process main is
    ** invert two  Integer numbers
    new x := 10;
    new y := 20;
    swap(@x, @y);
    expect (x == 20) and (y == 10);

    ** invert two Real numbers
    new a := 1.5;
    new b := 2.5;
    swap(@a, @b);
    expect (a == 2.5) and (b == 1.5);
  return;
end variant_params;

Date and Time

The types for dates, times, durations and the Quick Format moved to their own page: Eve Date and Time.

Data File Types

Proposed. The names and the forms of this section are a proposal for the author's review (Q-039). They are planned for the data language (level 5, version 0.4) and are not implemented.

A data client reads files: orders in a CSV, customers in a JSON, pages in HTML. Six types describe the content of such a file after it is loaded and parsed in memory. Each one is a class derived with <:, and most of them will be written in Eve itself, in the standard library, at a later time.

TypeThe fileA loop takes
JSONJSON textone element (an object, an array, a value) at a time
CSVfields separated by a character, usually with a header rowone row at a time
TXTfixed-length text: a simple text file where every field has a width (.txt)one row at a time
XMLXML textone element at a time
HTMLHTML textone element at a time
HTMTan HTML template: HTML with {expression} holes (Templates)one node of the template; rendering fills the holes

Buffered load and traversal

Opening a file does not read all of it. The value keeps a buffer: the parser reads a block of the file, gives its units one by one to a loop, and reads the next block when the buffer is empty. A file larger than the memory can be processed, and the memory used does not depend on the size of the file. The loop is the traversal: row by row for CSV and TXT, element by element (object by object) for JSON, XML and HTML.


from "lib" use (csv);
process main is
  new orders: CSV := csv.open("orders.csv");   ** nothing is read yet but the first block
  new sum := 0.0;
  for row in orders do                          ** one row at a time
    let sum += row["total"].real();
  done;
  print "total = {sum % f10.2}";
return;

A file that is not well formed raises an error when the loop reaches the bad unit, not when the file is opened: the units before it were already processed. The file is closed when the loop ends, also by break or by an error. The text of a unit is a String; a value becomes a number or a date only when the program asks for it.

All six types are classes derived from Document, which is an Iterable, so a for loop reads any of them. Typed reading with a record class, Unicode streams, rejects, reports and the canonical form are on the page Files and ETL (CSV, JSON and TXT in version 0.4, XML in 0.5, HTML and HTMT in 0.6).
TODO: a daily report that reads a CSV and fills an HTMT (see Templates).

Pitfalls

Real to Integer

A Real can't be assigned to an Integer: let a := 1.5; is an error when a is an Integer. Use floor, round or ceiling to choose how the decimals are removed.

Division

The operator / always returns a Real, also for 4 / 2. Enforce the type, as in new x = a / b :Integer;, to get an Integer.

Division by zero is always an error ($err_divide), also for Real numbers: Eve has no infinity and no NaN, so 1 / 0 and 1.0 / 0.0 stop the process unless it is recovered. A Rational with a zero denominator, 1\0, is not supported either.

The "is" operator

is does not compare values: use == for that. It checks a type, x is Integer. The same keyword can also introduce a block of code, so read the statement around it.

Range precision

The precision of a decimal range comes from the number of zeros in its limits: (0.0..1.0) and (0.00..1.00) are different ranges.