As programs grow, they should be broken into smaller pieces for maintainability. Janet has the concept of modules to this end. A module is a collection of bindings and it's associated environment. By default, modules correspond one to one with source files in Janet, although you may override this and structure modules however you like.
Installing a Module
path module can be installed like so from the command line:
sudo jpm install https://github.com/janet-lang/path.git
The use of
sudo is not required in some setups, but is often needed
for a global install on posix systems. You can pass in a git repository URL
to the install command, and jpm will install that package globally on your system.
If you are not using jpm, you can place the file
path.janet from the repository
in your current directory, and janet will be able to import it as well. However, for
more complicated packages or packages containing native C extension, jpm will usually
be much easier.
Importing a Module
To use a module, the best way is use the
(import) macro, which
looks for a module with the given name on your system and imports it's symbols
into the current environment, usually prefixed with the name of the module.
(import path) (path/join (os/cwd) "temp")
path is imported, all of it's symbols are available to the host program, but
path/. To import the symbols with a custom prefix or
without any prefix, use the
to the import macro.
(import path :prefix "") (join (os/cwd) "temp")
You may also use the
:as argument to specify the prefix in a more natural way.
(import path :as p) (p/join (os/cwd) "temp")
Custom Loaders (
module/loaders data structures determine how Janet
will load a given module name.
module/paths is a list of patterns and methods
to use to try and find a given module. The entries in
will be checked in order, as it is possible that multiple entries could match.
If a module name matches a pattern (which
usually means that some file exists), then we use the corresponding loader in
module/loaders to evaluate that file, source code, URL, or whatever else
we want to derive from the module name. The resulting value, usually an environment
table, is then cached so that it can be reused later without evaluating a module
module/loaders, you can create
custom module schemes, handlers for file extensions, or even your own module
system. It is recommended to not modify existing entries in
module/loader, and add on to the existing entries. This is rather
advanced usage, but can be incredibly useful in creating DSLs that feel
native to Janet.
This is an array of file paths to search for modules in the
file system. Each element in this array is a tuple
[path type predicate], where path
is the a templated file path, which determines what path corresponds to a module name, and
where type is the loading method used to load the module.
type can be one of
:image, or any key in the
predicate is an optional function
or file extension used to further filter whether or not a module name should match.
It's mainly useful when
path is a string and you want to further refine the pattern.
path can also be a function that checks if a module name matches. If the module name
matches, the function should return a string that will be used as the main identifier for the
module. Most of the time, this should be the absolute path to the module, but it can be
any unique key that identifies the module such as an absolute URL. It is this key that is
used to determine if a module has been loaded already.
This mechanism lets
refer to the same module even though they are different names passed to import.
Once a primary module identifier and module type has been chosen, Janet's import
machinery (defined mostly in
module/find) will use
the appropriate loader from
module/loaders to get an environment table.
Each loader in the table is just a function that takes the primary module identifier
(usually an absolute path to the module) as well as optionally any other arguments
import, and returns the environment table.
For example, the
:source type is a thin wrapper around
:image type is a wrapper around
load-image, and the
is a wrapper around
URL loader example
An example from
examples/urlloader.janet in the source repository shows how
a loader can be a wrapper around curl and
dofile to load source files from HTTP URLs.
To use it, simply evaluate this file somewhere in your program before you require code from
a URL. Don't use this as is in production code, as if
url contains spaces in
the module will not load correctly. A more robust solution would quote the URL, or better yet
os/execute and write to a temporary file instead of
(defn- load-url [url args] (def f (file/popen (string "curl " url))) (def res (dofile f :source url ;args)) (try (file/close f) ([err] nil)) res) (defn- check-http-url [path] (if (or (string/has-prefix? "http://" path) (string/has-prefix? "https://" path)) path)) # Add the module loader and path tuple to right places (array/push module/paths [check-http-url :janet-http]) (put module/loaders :janet-http load-url)
You can include files relative to the current file by prefixing
the module name with a
./. For example, if you have a file tree
that is structured like so:
mymod/ init.janet deps/ dep1.janet dep2.janet
With the above structure,
init.janet can import
dep2 with relative imports. This is less brittle as the entire
mymod/ directory can be installed without any chance that
dep2 will overwrite other files, or that
accidentally import a different file named
mymod can even be a sub directory in another janet
source tree and work as expected.
(import ./deps/dep1 :as dep1) (import ./deps/dep2 :as dep2) ...
Writing a module
Writing a module in Janet is mostly about exposing only the public functions
that you want users of your module to be able to use. All top level functions defined
defn, macros defined
defmacro, constants defined via
vars defined via
var will be exported in your module. To mark a function or
binding as private to your module, you may use
def- at the top level.
You can also add the
:private metadata to the binding.
# Put imports and other requisite code up here (def api-constant "Some constant." 1000) (def- private-constant "Not exported." :abc) (var *api-var* "Some API var. Be careful with these, dynamic bindings may be better." nil) (var *private-var* :private "var that is not exported." 123) (defn- private-fun "Sum three numbers." [x y z] (+ x y z)) (defn api-fun "Do a thing." [stuff] (+ 10 (private-fun stuff 1 2)))
To import our sample module given that it is in some path
could do something like the following (this also works in a repl):
(import mymod) mymod/api-constant # evaluates to 10000 (mymod/api-fun 10) # evaluates to 23 mymod/*api-var* # evaluates to nil (set mymod/*api-var* 10) mymod/*api-var* # evaluates to 10 (mymod/private-fun 10) # will not compile