Wiki

Clone wiki

helium / popl20 / Tutorial_Modules

Tutorial 4: Module system

Structures

In Helium we have sophisticated module system similar to one that can be found in OCaml or SML. The simpliest way to define a module is to define a structure that groups several definitions.

module B = struct
  data MyBool = MyTrue | MyFalse

  let ofBool b = if b then MyTrue else MyFalse

  let toBool m =
    match m with
    | MyTrue  => True
    | MyFalse => False
    end
end
We can refer to the elements of module B by providing a qualified name:
[IO]> B.toBool B.MyTrue ;;
False
Or we can open a module to make its contents visible in current scope.
let negB m =
  open B in
  match m with
  | MyTrue  => MyFalse
  | MyFalse => MyTrue
  end
Operation similar to opening is including. The difference is that definitions of included structure are also part of current scope. It only makes sense in definitions of structures. In the following example, the definitions from module List (e.g. map or length) are also part of module A, but they do not appear in module B.
module A = struct
  include List
  let length2 xs ys = length xs + length ys
end

module B = struct
  open List
  let length2 xs ys = length xs + length ys
end

Signatures and abstract types

The key feature of a module system is hiding of implementation details. It can be obtained by providing more specific signatures (types of modules) to a modules.

module AbstractB : sig
  type MyBool

  val ofBool : Bool -> MyBool
  val toBool : MyBool -> Bool
end = B
In this example, a module AbstractB contains type MyBool, but its implementation is not revealed. Such a type is called abstract. Refering to AbstractB.MyTrue would end up with a type error. We could exchange implementation of AbstractB to a different one, without affecting the rest of the program, e.g.,
module AbstractB : sig
  type MyBool

  val ofBool : Bool -> MyBool
  val toBool : MyBool -> Bool
end = struct
  type MyBool = Bool

  let ofBool b = b
  let toBool b = b
end
Using signatures we can hide some values provided by a module, or create abstract effects as well. Currently, all types declared in signatures must be abstract.

Source files as modules

Each source file of Helium with extension .he is in fact a body of structure definition. When you mention a name of module (e.g. Modl) in your source code and such a module is not locally defined nor visible in opened modules, then the interpreter would try to find source file with corresponding name (Modl.he) in the following locations (order matters):

  1. in the same directory,
  2. in directories passed to interpreter using -I option,
  3. in library directories, i.e., colon-separated list of directories stored in HELIUM_LIB environmental variable or lib/ when this variable is not defined.

Additionally, when the file is associated with file with .she file (e.g. Modl.she), the contents of .she file is treated as a body of signature for a module.

Modules as types

In larger projects, programmers often dedicate separate file for each datatype. With this aproach, types have two names: name of the module and name of a type inside a module. In Helium, we can share these two names, and refer to types by only the name of the module. When a module M contains type or effect named with keyword this, then this module can be used in a place where a type is expected.

module B = struct
  data this = MyTrue | MyFalse

  let ofBool b = if b then MyTrue else MyFalse
  let toBool (m : this) =
    match m with
    | MyTrue  => True
    | MyFalse => False
    end
end

let idB (b : B) : B = B.ofBool (B.toBool b)

Types as modules

Each datatype or effect is also visible for a module system as a separate module that contains type definition and constructors / fields / operations. This feature allows easly to avoid confusion, while defining several types with the same names of constructors in one module.

data TypeA = A | B
data TypeB = B | C

let pairOfB = (TypeA.B, TypeB.B)

TODO: include type

Functors

Updated