Eve Modules
Module declaration
A module is one script file with the extension ".eve". It begins with the keyword module, and its name is the name of the file without the extension. Everything between the header and end module_name; is indented by 2 spaces. A module has two optional regions: initialize, executed when the module is loaded, and finalize, executed when the driver ends.
Module skeleton:
# purpose of the module
module module_name is
** imports: a module can use other modules
from $path/library_name use (*);
** exports: the members other scripts may use (symmetric to use)
export (VERSION, Point, square, shift);
set VERSION = "1.0" :String; ** constant
class Point = {x, y :Real} <: Object; ** class
function square(x: Real) => (@result: Real) is
let result := x * x;
return;
** extension method: Point gets the method shift
method shift(@self: Point, dx, dy: Real) is
let self.x += dx;
let self.y += dy;
return;
** private members: not in the export list
new count = 0 :Integer; ** variable: always private
procedure trace(message: String) is ** no result: it changes count and prints
let count += 1;
print message;
return;
initialize
** executed once, when the module is loaded
...
finalize
** executed once, when the driver ends
...
end module_name;
Notes:
- A module has no process:
process main isbelongs to drivers and aspects. A module can't be applied; - The declarations can come in any order: they are hoisted. By convention: imports, exports, constants, variables, classes, functions and extension methods, then
initializeandfinalize; - At module level there are functions and extension methods; a method that is not an extension method belongs to a class (see Classes);
- Module names use lowercase letters, digits and underscore, no special characters and no Unicode. A name can be 30 characters long;
- A folder of modules is a library. Module names must be unique in a library.
Public and private members
A module exports the members that other scripts may use, in one list, export (VERSION, Point, square, shift);, symmetric to the use list of an import. A member that is not in the list is private: only the module itself can use it. The user of the module writes module_name.member, or imports the members without prefix with use (module_name(*)).
| Member | Declared with | Can be exported | Why |
|---|---|---|---|
| constant | set NAME = value :Type; | yes | a constant never changes, so every user can read it safely |
| class | class Name = {…} <: Object; | yes | a class is a type; its objects belong to the code that creates them |
| function | function name(…) => (…) is | yes | a plain function has no side effects |
| procedure | procedure name(…) is | yes | it does work; it may change the private state of the module |
| stochastic function | function name!(…) => (…) is | yes | its result can differ, for example because it reads the state of the module; it changes nothing |
| extension method | method name(@self: Class, …) is | yes | it adds a method to a class, for the scripts that import the module |
| variable | new name = value :Type; | no, only private | a module is shared by the whole program; an exported variable could be changed from anywhere, and the module could no longer keep its state right |
Extension methods. At module level, a method is always an extension method: its first parameter is @self with the type of a class, and it is called like a method of that class, p.shift(1, 2). An extension method can be used only inside the module or aspect that declares it. When a module exports it, every script that imports the module can use it.
export (shift);
** extension method: Point gets the method shift
method shift(@self: Point, dx, dy: Real) is
let self.x += dx;
let self.y += dy;
return;
A module has no public variables: a variable in the export list is an error. A value that other scripts need to change is passed to a function or a procedure of the module as a parameter.
A private variable of a module is written in its initialize region, and after that only by the procedures of the module, procedure tick() is … return;: a function changes nothing. A function that reads these variables is stochastic, because they change, so its name ends with !: function count!() => (@result: Integer). A plain function can't read them.
Modules are singletons
There is only one copy of a module in a running program:
- Loaded once: the first import loads the module. Every later import, by the driver, by an aspect or by another module, uses the same copy;
- Initialized once: the region
initializeruns once, right after the module is loaded, before the importer continues; - Finalized once: the region
finalizeruns once, when the driver ends, after the process of the driver has finished. Modules are finalized in the reverse order of their initialization; - No instances: a module is not a class.
new m := module_name();is an error. To have several objects with their own state, declare a public class in the module and create objects of it:new c := counter.Counter();.
Example: life cycle
# a module that counts its users
module counter is
export (LIMIT, tick, count!);
set LIMIT = 100 :Integer; ** constant
new total = 0 :Integer; ** private state
** no result: changes the private state
procedure tick() is
let total += 1;
return;
** stochastic: reads the private state, which can change
function count!() => (@result: Integer) is
let result := total;
return;
initialize
print "counter: loaded";
finalize
print "counter: {total} ticks";
end counter;
# use the module twice
driver count_demo is
from "lib" use (counter);
process main is
counter.tick();
counter.tick();
expect counter.count!() == 2;
expect counter.LIMIT == 100;
return;
end count_demo;
counter: loaded counter: 2 ticks
The line "counter: loaded" is printed once, at the import. The line "counter: 2 ticks" is printed once, after the process of the driver has returned.
Import
A driver, an aspect or a module imports modules with from … use. The path names the library folder. It is written without quotes, as folder names joined by / (lib/db, $EVE_LIB/db), or as a string expression when it is computed ("lib", $root_path/"lib"). A path that does not start with / is relative to $EVE_HOME, the home of the project (by default the folder of the driver): "lib" is $EVE_HOME/lib. A path that starts with / is absolute: "/test/eve1/lib". The list in parentheses says how the members are reached:
| Form | Imports | A member is used as |
|---|---|---|
use (m); | the module m | m.name |
use (m as x); | the module m, under the alias x | x.name |
use (m(*)); | the public members of m, into the scope (debug mode: a warning) | name, without prefix |
use (*); | every module of the folder, their names merged into the scope (debug mode: a warning) | name, without prefix |
Syntax:
driver script_name is
from lib/db use (core, oracle as orcl); ** core.name, orcl.name
from $EVE_LIB/util use (text(*)); ** the public members of text, without prefix
from "lib" use (*); ** every module of lib, names merged
** an alias for one public member
def new_name = core.member_name;
process main is
...
return;
end script_name;
Notes:
- With
use (m)or an alias, two modules can have members with the same name without conflict; - Members merged with
(*)orm(*)must have different names: two members with the same name make the import fail. Then list the modules one by one, withm(*)for some andmor an alias for the others. This is how the standard modules give the language statements such asprint; - A module knows nothing of the script that imports it: it can't read the globals of the driver. Values go in and out through the parameters of its methods and functions;
- A module is searched in this order: the standard library; the folder
$EVE_LIB, which is thelibfolder of the project; then each folder of$EVE_LIB_PATH, where external modules are installed (see Library below). If the module is not found, the import fails with the error$err_module(code 30): the program ends with exit code 4, because nobody can recover an error that happens before the process starts; - Circular imports are possible: a module that is being loaded is not loaded again;
- The standard modules, such as
ioandexception, form the system library: a fixed list, imported by default, with nouse. Every other library is imported by name. In debug mode the compiler warns on the forms that bring names without a prefix,(m(*))and(*), because a name of the library can later hide or collide with a local name.
Library modules
The standard library of Eve is a set of modules written in Eve. A function or method that needs the virtual machine is declared with the keyword external: the module keeps only its signature, and the body is implemented by the machine. For example the module io:
# standard output and error
module io is
** write the arguments at the current position of stdout
external write(*args: String, sep := "", eol := False);
** write the arguments to stdout, then a new line
external print(*args: String, sep := ",");
** write an error message to stderr
external error(message: String);
end io;
The comment lines above a declaration are its documentation: the command eve --doc generates the library reference from them. See Standard Library and Exceptions for the modules io and exception.
Library
A library is a set of reusable modules. A library can be installed in EVE environment or can be project specific. EVE machine is using two system variables to search for a library: $EVE_LIB is the lib folder of the project, and $EVE_LIB_PATH is a list of folders, separated like the PATH of the operating system, where external modules are installed. They can be in different folders, for example /eve/modules/<module_name>, installed from GitHub with npm install. A relative folder is relative to $EVE_HOME, the home of the project, and a folder that starts with / is absolute. If the library is not found the module can't be imported and the script fails with $err_module.
- libraries contain generic functionality and can be shared between multiple projects;
- using import, several modules can be loaded from a library one by one or all using (*);
- circular import is possible, but EVE prevent infinite recursive reference;
- after import you can call public members of a module using dot notation;
Note: A library is a folder of module script files. A module is one script file and its name is the file name without the extension. By convention you can create intermediate subfolders but only the last folder is the library and module names must be unique in a library.
Scripts compared
| Driver | Aspect | Module | |
|---|---|---|---|
| Header | driver name is | aspect name is | module name is |
| Executable | yes: process main, run from the command line | yes: process main, run with apply | no process; initialize and finalize |
| Copies at run time | one, the program | one for each call; the state is dropped when main returns | one, a singleton, from the first import to the end of the driver |
| Instances (class call) | no | no | no |
| Public members | none | none | the export list: constants, classes, functions, extension methods; no variables |
| Global variables | yes, the only script with globals | no: private state for one call | private variables, shared by all users |
| Can import modules | yes | yes | yes |
Read next: Subprograms