8.1 Module Basics
Each Rhombus module typically resides in its own file. For example, suppose the file "cake.rhm" contains the following module:
"cake.rhm"
print_cake
// draws a cake with `n` candles
layer(" ", n, Char".", " ")
layer(" .-", n, Char"|", "-. ")
layer(" | ", n, Char" ", " | ")
layer("---", n, Char"-", "---")
fun layer(pre, n, ch, post):
println(pre ++ String.make(n, ch) ++ post)
Then, other modules can import "cake.rhm" to use the print_cake function, since the export declaration in "cake.rhm" explicitly exports the definition print_cake. The layer function is private to "cake.rhm" (i.e., it cannot be used from other modules), since layer is not exported.
The following "random_cake.rhm" module imports "cake.rhm":
"random_cake.rhm"
"cake.rhm" open
print_cake(math.random(30))
The relative reference "cake.rhm" with import works if the "cake.rhm" and "random_cake.rhm" modules are in the same directory. Unix-style relative paths are used for relative module references on all platforms, much like relative URLs in HTML pages.
8.1.1 Organizing Modules
The "cake.rhm" and "random_cake.rhm" example demonstrates the most common way to organize a program into modules: put all module files in a single directory (perhaps with subdirectories), and then have the modules reference each other through relative paths. A directory of modules can act as a project, since it can be moved around on the filesystem or copied to other machines, and relative paths preserve the connections among modules.
As another example, if you are building a candy-sorting program, you might have a main "sort.rhm" module that uses other modules to access a candy database and a control sorting machine. If the candy-database module itself is organized into sub-modules that handle barcode and manufacturer information, then the database module could be "db/lookup.rhm" that uses helper modules "db/barcodes.rhm" and "db/makers.rhm". Similarly, the sorting-machine driver "machine/control.rhm" might use helper modules "machine/sensors.rhm" and "machine/actuators.rhm".
The "sort.rhm" module uses the relative paths "db/lookup.rhm" and "machine/control.rhm" to import from the database and machine-control libraries:
"sort.rhm"
"db/lookup.rhm"
"machine/control.rhm"
....
The "db/lookup.rhm" module similarly uses paths relative to its own source to access the "db/barcodes.rhm" and "db/makers.rhm" modules:
"db/lookup.rhm"
"barcodes.rhm"
"makers.rhm"
....
Ditto for "machine/control.rhm":
"machine/control.rhm"
"sensors.rhm"
"actuators.rhm"
....
Racket tools all work automatically with relative paths. For example,
rhombus sort.rhm
on the command line runs the "sort.rhm" program and automatically compiles and loads imported modules. You can also run with racket instead of rhombus, but racket does not automatically recompile as needed or save compiled modules unless you supply the -y flag.
To compile without running so that a later use of rhombus or racket will not need to compile, use raco make:
raco make sort.rhm
8.1.2 Library Collections
A collection is a hierarchical grouping of installed library modules. A module in a collection is referenced through an unquoted, suffixless path. For example, the following module refers to the "date.rhm" library that is part of the "rhombus" collection:
Technically, / is an operator that joins the rhombus and date identifiers to form the module path rhombus/date.
When you search the Rhombus documentation, search results indicate the module that provides each binding. Alternatively, if you reach a binding’s documentation by clicking on hyperlinks, you can hover over the binding name to find out which modules provide it.
A module reference like rhombus/date looks like a division expression, but it is not treated as an expression. Instead, when import sees a module reference that is unquoted, it converts the reference to a collection-based module path:
First, if the unquoted path contains no /, then import automatically adds a /main to the end. For example, import slideshow is equivalent to import slideshow/main.
Second, import implicitly adds a ".rhm" suffix to the path while treating its a lib form, so import slideshow/main is equivalent to import lib("slideshow/main.rhm").
Finally, import resolves a lib module path by searching among installed collections, instead of treating the path as relative to the enclosing module’s path.
To a first approximation, a collection is implemented as a filesystem directory. For example, the "racket" collection is mostly located in a "racket" directory within the installation’s "collects" directory.
The installation’s "collects" directory, however, is only one place that import looks for collection directories. Other places include a subdirectory of the user-specific directory reported by system.path(#'addon_dir) and directories configured through the PLTCOLLECTS search path. Finally, and most typically, collections are found through installed packages.
8.1.3 Packages and Collections
A package is a set of libraries that are installed through the Racket package manager (or included as pre-installed in a Racket or Rhombus distribution). For example, the gui library is provided by the "rhombus-gui" package, while draw is provided by the "rhombus-draw" package.More precisely, gui is provided by "rhombus-gui-lib", draw is provided by "rhombus-draw-lib", and the "rhombus-gui" and "rhombus-draw" packages extend "rhombus-gui-lib" and "rhombus-draw-lib" with documentation.
Rhombus programs do not refer to packages directly. Instead, programs refer to libraries via collections, and adding or removing a package changes the set of collection-based libraries that are available. A single package can supply libraries in multiple collections, and two different packages can supply libraries in the same collection (but not the same libraries, and the package manager ensures that installed packages do not conflict at that level).
The distinction between packages and modules is unlike some package manages for specific programming languages, but it is similar to many operating-system package managers. A package provides a set of library modules, but there’s no requirement that the modules have a name that is based on the package name. A package can add to multiple collections, and multiple packages can add to the same collection. For example, the rhombus-ssl-lib package provides a net/ssl module, even though net is not in the package name. The rhombus-url-lib package similarly provides net/url, also adding to the "net" collection. The package manager ensures that full library names do conflict. Much like operating-system packages, updates to a package are generally intended to be backward-compatible, and API changes are reflected by increasing a number that is part of the package name.
For more information about packages, see Package Management in Racket.
8.1.4 Adding Collections
Looking back at the candy-sorting example of Organizing Modules, suppose that modules in "db/" and "machine/" need a common set of helper functions. Helper functions could be put in a "utils/" directory, and modules in "db/" or "machine/" could access utility modules with relative paths that start "../utils/". As long as a set of modules work together in a single project, it’s best to stick with relative paths. A programmer can follow relative-path references without knowing about your Rhombus configuration.
Some libraries are meant to be used across multiple projects, so that keeping the library source in a directory with its uses does not make sense. In that case, the best option is to add a new collection. After the library is in a collection, it can be referenced with an unquoted path, just like libraries that are included with the Rhombus distribution.
You could add a new collection by placing files in the Racket installation. Alternatively, you could add to the list of searched directories by setting the PLTCOLLECTS environment variable.If you set PLTCOLLECTS, include an empty path by starting the value with a colon (Unix and Mac OS) or semicolon (Windows) so that the original search paths are preserved. The best option, however, is to add a package.
Creating a package does not mean that you have to register with a package server or perform a bundling step that copies your source code into an archive format. Creating a package can simply mean using the package manager to make your libraries locally accessible as a collection from their current source locations.
For example, suppose you have a directory "/usr/molly/bakery" that contains the "cake.rhm" module (from the beginning of this section) and other related modules. To make the modules available as a "bakery" collection, either
Use the raco pkg command-line tool:
raco pkg install --link /usr/molly/bakery
where the --link flag is not actually needed when the provided path includes a directory separator.
Use DrRacket’s Package Manager item from the File menu. In the Do What I Mean panel, click Browse..., choose the "/usr/molly/bakery" directory, and click Install.
Afterward, import: bakery/cake from any module will import the print_cake function from "/usr/molly/bakery/cake.rhm".
By default, the name of the directory that you install is used both as the package name and as the collection that is provided by the package. Also, the package manager normally defaults to installation only for the current user, as opposed to all users of a Racket installation. See Package Management in Racket for more information.
If you intend to distribute your libraries to others, choose collection and package names carefully. The collection namespace is hierarchical, but top-level collection names are global, and the package namespace is flat. Consider putting one-off libraries under some top-level name like "molly" that identifies the producer. Use a collection name like "bakery" when producing the definitive collection of baked-goods libraries.
After your libraries are put in a collection you can still use raco make to compile the library sources, but it’s better and more convenient to use raco setup. The raco setup command takes a collection name (as opposed to a file name) and compiles all libraries within the collection. In addition, raco setup can build documentation for the collection and add it to the documentation index, as specified by an "info.rkt" module in the collection. See raco setup: Installation Management for more information on raco setup. Note that you will have to use Racket syntax for some of those pieces; collection information is always in "info.rkt", and "info.rhm" is not recognized.
8.1.5 Module References Within a Collection
When a module within a collection references another module within the same collection, either a relative path or a collection path could work. For example, a "sort.rhm" module that references "db/lookup.rhm" and "machine/control.rhm" modules within the same collection could be written with relative paths as in Organizing Modules:
"sort.rhm"
"db/lookup.rhm"
"machine/control.rhm"
....
Alternatively, if the collection is named "candy", then "sort.rhm" could use collection paths to import the two modules:
"sort.rhm"
....
For most purposes, these choices will work the same, but there are exceptions. When writing documentation with Scribble, you must use a collection path with docmodule and similar forms; that’s partly because documentation is meant to be read by client programmers, and so the collection-based name should appear. Meanwhile, for import, using relative paths for references within a collection tends to be the most flexible approach, but with caveats.
Relative-path references work much like relative URL references: the reference is expanded based on the way the enclosing module is accessed. If the enclosing module is accessed through a filesystem path, then a relative path in import is combined with that filesystem path to form a new filesystem path. If the enclosing module is accessed through a collection path, then a relative path in import is combined with that collection path to form a new collection path. A collection path is, in turn, converted to a filesystem path, and so the difference between starting with a filesystem or collection path does not usually matter. Unfortunately, inherent complexities of path resolution can create differences in some situations:
Through soft links, multiple mount points, or case-insensitive filesystems (on an operating system that does not implicitly case-normalize paths), there may be multiple filesystem paths that refer to the same module file.
For example, when the current directory is the "candy" collection’s directory, the current-directory path that rhombus receives on startup may cause rhombus sort.rhm to use a different filesystem path than racket -l candy/sort finds through the library-collection search path. In that case, if "sort.rhm" leads to some modules through both relative-path references and collection-based references, it’s possible that those resolve to different instances of the same source module, creating confusion through multiple instantiations.
When raco exe plus raco distribute are used to create an executable to run on a different machine, the paths of the current machine are likely unrelated to paths on the target machine. The raco exe tool treats modules that are referenced via filesystem paths differently than modules referenced via collection paths, because only the latter make sense to access through reflective operations at run time.
For example, if raco exe sort.rhm creates an executable that uses dynamic_import(ModulePath'candy/db/lookup', #'all) at run time, then that dynamic_import will fail in the case that "db/lookup.rhm" is resolved relative to the filesystem path "sort.rhm" at executable-creation time.
Using only collection-based paths (including using shell commands like racket -l candy/sort and not like rhombus sort.rhm) can avoid all problems, but then you must only develop modules within an installed collection, which is often inconvenient. Using relative-path references consistently tends to be the most convenient while still working in most circumstances.
