Defines the MODULE function, that creates encapsulated "modules" that are isolated from the rest of the code. Also defines LOAD-MODULE to automate module loading.
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.
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〉
]
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
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
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〉
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
]
]
〈module's locals〉 ≡
result imports' globals'
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
〈Check if value is a module object〉 ≡
to logic! all [
object? get/any 'value
'module = get in value 'type
]
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
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:
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
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
]
]
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
]
Finally, we do the body, and return result.
〈do the body; return the module object〉 ≡
do body
result
〈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: [ ]
〈load-module's locals〉 ≡
loaded search-path save-dir
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)]
]
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〉
]
This is an example of how to inspect a module object.
〈How to inspect a module〉 ≡
; TODO
Thanks to Brian Hawley and Gregg Irwin for their help during the design phase.