Contents:

1. Introduction

In a larger project with many people contributing to the code, it is useful to provide mechanisms to better isolate parts of the code from each other, so that mistakes, typos, etc. in some part will have a smaller chance of affecting the other parts. For this reason, we define here a module function that creates an "isolated" piece of code, that only interacts with the rest of the system according to the provided specification.

2. Overview

The module! object is the prototype for modules; we represent them as objects so that it is possible to inspect them after creation (see How to inspect a module). The module function creates such objects and does all the magic necessary for the code in the body to be isolated from the rest of the system.

The spec argument is a block which can specify the title of the module (Title: "Some string"), the description or purpose of the module (Purpose: "Some string"), and how the module interacts with the rest of the system. In particular, you can specify whether you want access to the words exported by another module by importing it with Imports: [block of words or files] (each word should refer to a module object; file! values are passed to load-module, and the resulting module is then imported; it is possible to use strings or other values in this block to provide comments). You can also specify which words you want other modules to be able to import from you by exporting them using Exports: [block of words] (you can also use strings or other values in this block to provide comments about the words you are exporting). It is also possible to use Exports: 'All to signal that all the module's locals (that is, all the contained set-word!'s) should be exported. Finally, you can allow your module to alter the global context in a controlled way, by using Globals: [block of words] (again, strings are ok in this block); such words will not be made local to the module.

The module? function returns true if the passed value is a module object.

See Examples for some examples.

Overview

Support functions

module!: context [
 Definition of the module! object prototype
]

module: func [
 "Create a new module" [catch]
 spec [block! object!] {You can set TITLE, PURPOSE, IMPORTS, EXPORTS, GLOBALS}
 body [block!]

 /local module's locals
] [
 Create a module based on the given spec and body
]

module?: func [
 "Returns TRUE for module objects"
 value [any-type!]
] [
 Check if value is a module object
]

We also define load-module, that can load a REBOL script as a module based on its header. The header must have Type set to 'module, and can specify Imports, Exports and Globals with the meanings given above. Unless the argument is a url! or an absolute path, the module is searched for in the search path (that can be specified using the /from refinement). Please note that you have to add the current directory (%./) to the search path explicitly if you want to allow the argument to be relative to the current directory.

load-module never loads the same module (identified by its absolute path) twice. Also, since the module function automatically calls load-module on the modules to import, this means that when you load a module all the other modules it depends on are also loaded automatically.

Overview +≡

load-module: func [
 "Load a REBOL script as a module" [catch]
 script [file! url!]
 /from "Add SCRIPT to the search path"

 /local load-module's locals
] [
 Load script as a module
]

3. Examples

Create a new module:

Examples

my-module: module [
 Title: "My module"
 Purpose: "This is my module. It is just an example module."
 Imports: [
  my-other-module "We use some stuff from my-other-module"
  %modules/foo.r {Call LOAD-MODULE on the file, and import the result}
 ]
 Exports: [
  f "You can use this"
  g "Be careful with it"
 ]
 Globals: [
  a "We set this globally."
 ]
] [
 a: 1 ; set globally as specified above
 b: 2 ; local to the module
 f: does [ ; exported to other modules
  print 'f
  c: 3 ; will also be local to the module
 ]
 g: does [ ; exported to other modules
  print 'g
  print "careful!"
  h
 ]
 h: does [ ; local to this module
  print 'h
 ]
]

Set the search path, and load modules relative to it:

Examples +≡

load-module/from %my-modules/
load-module %my-module.r

But you can also specify a full path, thus ignoring the search path:

load-module %/path/to/my-module.r
load-module http://my-modules.com/my-module.r

4. Definition of the module! object prototype

This is the prototype used for module objects. imported, exports and global are the blocks as they were provided in the spec for the module. (Note that they can be none if they were not specified in the spec.) local-ctx and export-ctx are the contexts used by the module, and you should not touch them.

Definition of the module! object prototype

Type: 'module
Title: "Untitled"
Purpose: "Undocumented"
Imports: Exports: Globals: none
local-ctx: export-ctx: none

5. Create a module based on the given spec and body

These are the steps required to create a module:

Create a module based on the given spec and body

Create the module object from the spec
Create the list of local words
Import other modules specified in the spec
Find words that are global or imported from another module, but are being set in the body
Create the local and export contexts and bind the body to them
Import the values that would otherwise be shielded by the local context
do the body; return the module object

5.1 Create the module object from the spec

Here we make the result module object from the given spec block. We also prepare export-ctx, imports' and globals' by extracting all the words from exports, imports and globals.

Create the module object from the spec

result: make module! spec
; we can't allow any-word for globals because EXCLUDE (used below)
; distinguishes between different types of words
foreach [src dst cond] bind [
 exports export-ctx [not word? :value]
 globals globals' [not word? :value]
 imports imports' [not any [any-word? :value file? :value url? :value]]
] result [
 unless 'All = get src [
  set dst unique any [get src [ ]]
  remove-each value get dst cond
 ]
]

5.2 module's locals

module's locals

result imports' globals'

5.3 Create the list of local words

Here we parse the body deeply for set-word!'s. We list all words that need to be made local in result/local-ctx, and exclude the words that we want global from them. (Notice that we don't need to copy the block here, because exclude will, and we're going to make a context out of it anyway.) We also handle the case where Exports: was set to 'All here, making the local context empty and putting all words in the export context.

Notice that we just leave the words in the globals' list alone, so they may be bound to other contexts as well, not just system/words.

Create the list of local words

result/local-ctx: clear []
parse body rule: [
 any [
  ; need to word! here because EXCLUDE (used below)
  ; distinguishes between different types of words
  set word set-word! (append result/local-ctx to word! word)
  |
  into rule
  |
  skip
 ]
]
either result/exports = 'All [
 result/export-ctx: exclude result/local-ctx globals'
 clear result/local-ctx
] [
 result/local-ctx: exclude result/local-ctx append copy globals' result/export-ctx
]

We need these additional local words for the function:

module's locals +≡

rule word

5.4 Check if value is a module object

Check if value is a module object

to logic! all [
 object? get/any 'value
 'module = get in value 'type
]

5.5 Import other modules specified in the spec

Here we iterate thru all the values in the imports' list, call load-module or check to make sure that they refer to a module object, and finally bind the body to their export context, in the order they are specified.

We are also binding the result/local-ctx list of words to their export context; we are doing this in preparation for the Import the values that would otherwise be shielded by the local context step.

Import other modules specified in the spec

foreach module-name imports' [
 set/any 'module either word? module-name [
  get/any module-name
 ] [
  load-module module-name
 ]
 either module? get/any 'module [
  if object? module/export-ctx [
   bind body module/export-ctx
   ; used to resolve conflicts
   bind result/local-ctx module/export-ctx
  ]
 ] [
  throw make error! join "Not a module: " module-name
 ]
]

module needs to be made local:

module's locals +≡

module

5.6 Find words that are global or imported from another module, but are being set in the body

This step is necessary to avoid a problem. Since we make all words that are being set local, regardless of where they are being set, and regardless of whether we are also importing the same word from another module, we end up "shielding" access to those words. On one side, we want this to happen (since we don't want to allow the module to alter a module it is importing, or the global context), but on the other side we don't want to prevent access to the values these words refer to.

A word in result/local-ctx has a value in the following cases:

  1. it has a value in the global context;
  2. it has a value in the export context of one of the modules we are importing (this is thanks to the bind we do in Import other modules specified in the spec).

We collect such words in the conflicts block.

Find words that are global or imported from another module, but are being set in the body

conflicts: clear [ ]
foreach word result/local-ctx [
 if value? word [append conflicts word]
]

conflicts needs to be local:

module's locals +≡

conflicts

5.7 Create the local and export contexts and bind the body to them

We create the local and export contexts using make-context, and bind the body to them.

Create the local and export contexts and bind the body to them

foreach ctx bind [local-ctx export-ctx] result [
 either empty? get ctx [
  set ctx none
 ] [
  bind body set ctx make-context get ctx
 ]
]

5.8 Import the values that would otherwise be shielded by the local context

This step solves the problem we described in Find words that are global or imported from another module, but are being set in the body, by importing the values in the local context.

Import the values that would otherwise be shielded by the local context

foreach word conflicts [
 set in result/local-ctx word get word
]

5.9 do the body; return the module object

Finally, we do the body, and return result.

do the body; return the module object

do body
result

6. Load script as a module

Load script as a module

search-path: [ ]
if from [return append search-path script]
either any [url? script #"/" = first script] [
 if loaded: select modules script [return loaded]
 parse loaded: load/header script [
  'REBOL block! (
   loaded: next loaded
   loaded/1: construct loaded/1
  )
 ]
 unless module? loaded/1 [
  throw make error! rejoin ["Not a module: " mold script]
 ]
] [
 loaded: foreach path search-path [
  if loaded: select modules path/:script [return loaded]
  if exists? path/:script [
   loaded: load/header path/:script
   unless module? loaded/1 [
    throw make error! rejoin ["Not a module: " mold script]
   ]
   script: path/:script
   break/return loaded
  ]
 ]
 unless loaded [
  throw make error! rejoin ["Cannot find " mold script]
 ]
]
if find modules script [
 throw make error! rejoin ["Loading loop detected: aborted at " mold script]
]
repend modules [script none]
unless url? script [
 save-dir: what-dir
 change-dir first split-path script
]
loaded: module loaded/1 next loaded
unless url? script [
 change-dir save-dir
]
poke find modules script 2 loaded
loaded

List of loaded modules:

Overview +≡

modules: [ ]

6.1 load-module's locals

load-module's locals

loaded search-path save-dir

7. Support functions

The make-context function creates a context from a list of words. The context is returned as an object, but notice that it has no 'self word.

Support functions

make-context: func [words] [
 use words compose [bind? (to lit-word! first words)]
]

8. Hiding the support functions from the global context

We don't want to make make-context and modules global, so the main code block is actually using use to keep it local. (We cannot use module while defining it, can we?)

Hiding the support functions from the global context

use [make-context modules] [
 Overview
]

9. How to inspect a module

This is an example of how to inspect a module object.

How to inspect a module

; TODO

10. Credits

Thanks to Brian Hawley and Gregg Irwin for their help during the design phase.