Data Types
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.
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.
| Type | Zero value |
|---|---|
| Byte, Short, Integer, Natural, Huge | 0 |
| Real, Float | 0.0 |
| Decimal | 0d |
| Rune | nil, the empty rune '' |
| Logic | False |
| Ordinal | its first element |
| Time, Duration | 0 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, record | an 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.
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
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;.
| Suffix | Type | Example |
|---|---|---|
| none | Integer, or Real when the literal has a point | 42, 3.14 |
d | Decimal | 12.50d |
r | Real (double precision) | 5r |
f | Float (single precision) | 1.5f |
b | Byte | 255b; 0b is the Byte zero, 0b0 the binary literal zero |
w | Short (word) | 11w |
n | Natural | 42n |
z | Huge | -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 plainIntegernever holdsnull; - 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
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.
| Type | The file | A loop takes |
|---|---|---|
JSON | JSON text | one element (an object, an array, a value) at a time |
CSV | fields separated by a character, usually with a header row | one row at a time |
TXT | fixed-length text: a simple text file where every field has a width (.txt) | one row at a time |
XML | XML text | one element at a time |
HTML | HTML text | one element at a time |
HTMT | an 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.
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).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.