Eve Modules

A module is a script file that holds reusable code: constants, classes, functions and extension methods. It is not executable: it has no process and it can't be applied. A module is a singleton: it is loaded once, initialized once and finalized once, and you can't create instances of it. Drivers, aspects and other modules import it and use its public members with the dot operator. This chapter explains the declaration of a module, its public and private members, its life cycle, the import and the library.

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 is belongs 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 initialize and finalize;
  • 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(*)).

MemberDeclared withCan be exportedWhy
constantset NAME = value :Type;yesa constant never changes, so every user can read it safely
classclass Name = {…} <: Object;yesa class is a type; its objects belong to the code that creates them
functionfunction name(…) => (…) isyesa plain function has no side effects
procedureprocedure name(…) isyesit does work; it may change the private state of the module
stochastic functionfunction name!(…) => (…) isyesits result can differ, for example because it reads the state of the module; it changes nothing
extension methodmethod name(@self: Class, …) isyesit adds a method to a class, for the scripts that import the module
variablenew name = value :Type;no, only privatea 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 initialize runs once, right after the module is loaded, before the importer continues;
  • Finalized once: the region finalize runs 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:

FormImportsA member is used as
use (m);the module mm.name
use (m as x);the module m, under the alias xx.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 (*) or m(*) must have different names: two members with the same name make the import fail. Then list the modules one by one, with m(*) for some and m or an alias for the others. This is how the standard modules give the language statements such as print;
  • 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 the lib folder 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 io and exception, form the system library: a fixed list, imported by default, with no use. 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

DriverAspectModule
Headerdriver name isaspect name ismodule name is
Executableyes: process main, run from the command lineyes: process main, run with applyno process; initialize and finalize
Copies at run timeone, the programone for each call; the state is dropped when main returnsone, a singleton, from the first import to the end of the driver
Instances (class call)nonono
Public membersnonenonethe export list: constants, classes, functions, extension methods; no variables
Global variablesyes, the only script with globalsno: private state for one callprivate variables, shared by all users
Can import modulesyesyesyes

Read next: Subprograms