On this page:
7.1 Modules and Macros
7.2 Modules and Spaces
7.3 Modules and Languages
7.4 Exercise
8.18.0.18

7 Whole Languages🔗

Throughout the tutorial, we have appealed to predefined elements of Rhombus as examples of the kind of extensibility that Rhombus enables. The difference between Rhombus functionality and the kinds of macros that we have written is that Rhombus forms are packaged into a language that you can use with #lang.

Packaging a set of macros into a form for use with #lang in the same way as rhombus requires two steps:

  • defining a module that exports all of the bindings that are in the language, including implicit forms; and

  • making the module part of your Racket installation, typically through a package, since the name after #lang is resolved to a implementing module through the installation.

We’ll consider the first step here, but we’ll leave details of the second step to the Rhombus documentation.

7.1 Modules and Macros🔗

Each Rhombus source file that starts with #lang rhombus defines a module. A module can export bindings for use in other modules using export, and other modules reference the exporting module using import. In the case of modules that are adjacent in the filesystem, they can refer to each other via relative-path strings.

For example, if "fib.rhm" contains

"fib.rhm"

#lang rhombus

 

export:

  fib

 

fun fib(n):

  match n

  | 0 || 1: 1

  | n: fib(n-1) + fib(n-2)

then "main.rhm" in the same directory can use "fib.rhm" like this:

"main.rhm"

#lang rhombus

 

import:

  "fib.rhm" open

 

fib(5)

Macros can be exported just the same as functions. For example, if you start with the solution my_operator_soln.rhm to an earlier exercise, then you can use my_operator as exported from that module:

"main.rhm"

#lang rhombus

 

import:

  "my_operator_soln.rhm" open

 

my_operator a <!!!> b:

  [a && b, a || b]

 

#true <!!!> #false

The my_operaor macro expands to a use of expr.macro, which is not directly accessible in "main.rhm", since it does not import rhombus/meta or use #lang rhombus/and_meta. the import my_operaor macro works, anyway, because syntax objects retain scope information and enable hygienic macros.

7.2 Modules and Spaces🔗

Suppose that we want to use interp and prog of interp_space.rhm (from an earlier exercise) in "main.rhm":

"main.rhm"

#lang rhombus

 

import:

  "interp_space.rhm" open

 

interp(prog: 1 + 2,

       {})

Clearly, we’ll need to adjust "interp_space.rhm" to export interp and prog:

export:

  interp

  prog

This turns out not to be enough, however. After adding this export, attempting to run "main.rhm" reports an error about a missing #%literal for the 1 in prog: 1 + 2. The missing #%literal is in the one in the lc space. It will turn out that the + for lc is also not available.

To fix the problem, put an additional export form after the definition of the lc space "interp_space.rhm" (about 2/3 of the way down from the top of the file):

export:

  only_space lc:

    +

    ==

    let

    fun

    #%call

    #%parens

    #%literal

This example illustrates how Rhombus provides fine-grained control over the bindings that are accessible from a module. To be able to import these bindings, however, we needed at least import from the rhombus language. By creating a module that can be referenced through a #lang line, we can create a language that is smaller than rhombus.

7.3 Modules and Languages🔗

We will not be able to write #lang "interp_space.rhm", because quoted relative paths are not allowed after #lang. Fortunately, the shrubbery language can give us a little help. When #lang shrubbery is by itself on a line, then it works the way we used in an earlier exercise. But when shrubbery is followed by a quoted path as in #lang shrubbery "interp_space.rhm", then it imports the path for use in the module body, instead of just printing the body’s parsed shrubbery form.

Our goal is to make this "main.rhm" module work by using the prog parser and interp function from "interp_space.rhm" instead of compiling it as Rhombus code. Again, writing an interpreter is not really the best way to create a language in Rhombus. You should just compile your language directly to Rhombus code via macros. We continue to use the interpreter example here, anyway.

"main.rhm"

#lang shrubbery "interp_space.rhm"

 

let x = 1:

  let f = (fun (y): y + 2):

    f(x) * 3

Attempting to run this new "main.rhm" will fail, however, with an error message about a missing #%module-begin binding. As the #% prefix suggests, #%module-begin is an implicit that wraps a module body. It turns out that #%module-begin is from the Racket layer of Rhombus, and it works in Racket-native terms. Rhombus has its own #%module_block protocol that works in Rhombus-native terms, and Rhombus exports a #%module-begin that bridges to #%module_block. The Rhombus spelling of the Racket #%module-begin identifier (which has a hyphen) is #{#%module-begin}.

So, to make the new "main.rhm" work, we need to change "interp_space.rhm" to

Here’s the decl.macro and export form to add to "interp_space.rhm":

export:

  #{#%module-begin}

  rename:

    my_module_block as #%module_block

 

decl.macro 'my_module_block:

              $body':

  '#%module_block:

     interp(prog: $body,

            {})'

The exported #%module_block macro is defined as my_module_block so that it can expand to a use of the Rhombus #%module_block form, and then it is renamed to #%module_block on export.

7.4 Exercise🔗

Change the my_module_block form in "interp_space.rhm" so that it allows multiple groups in the module body, and it independently interps and prints a result for each of them.