Janet 1.27.0-01aab66 Documentation
(Other Versions:
1.26.0
1.25.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.0
1.18.1
1.17.1
1.16.1
1.15.0
1.13.1
1.12.2
1.11.1
1.10.1
1.9.1
1.8.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.1
)
Modules
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 its 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
Using jpm
, the 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
extensions, 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 its symbols into the
current environment, usually prefixed with the name of the module.
(import path)
(path/join (os/cwd) "temp")
Once path
is imported, all of its symbols are available to the host
program, but prefixed with path/
. To import the symbols with a custom
prefix or without any prefix, use the :prefix
argument 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/paths
and module/loaders
)
The module/paths
and 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
module/paths
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 twice.
By modifying module/paths
and 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/paths
or module/loader
, and simply add to the existing entries. This is rather
advanced usage, but can be incredibly useful in creating DSLs that feel native
to Janet.
module/paths
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 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 :native
, :source
, :image
, or any key in the
module/loaders
table.
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
./mymod
and mymod
refer to the same module even though they
are different names passed to import
.
module/loaders
Once a primary module identifier and module type has been chosen, Janet's import
machinery (defined mostly in require
and 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 passed to require
or import
, and returns the environment
table. For example, the :source
type is a thin wrapper around
dofile
, the :image
type is a wrapper around load-image
, and
the :native
type is a wrapper around native
.
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 load-url
the module will not load
correctly. A more robust solution would quote the URL, or better yet use
os/execute
and write to a temporary file instead of file/popen
.
(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)
Relative imports
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 dep1
and
dep2
with relative imports. This is less brittle as the entire
mymod/
directory can be installed without any chance that dep1
and
dep2
will overwrite other files, or that init.janet
will
accidentally import a different file named dep1.janet
or
dep2.janet
. mymod
can even be a sub-directory in another Janet
source tree and work as expected.
init.janet
(import ./deps/dep1 :as dep1)
(import ./deps/dep2 :as dep2)
...
Note that relative imports are relative to the current file, not to the working directory. For that behavior, see the section on working directory imports.
Pre-loaded Modules
An easy way to bypass the import system and create custom modules is to manually add modules into
module/cache
.
(put module/cache "mymod" (dofile "localfile.janet"))
# The module defined by localfile.janet can now be
# imported with (import mymod)
This is not recommended except in a few circumstances where a module needs to be loaded at runtime
but doesn't exist on disk, such as if a program were compiled into a standalone executable. A
pattern to allow this in a standalone executable built with jpm
is as follows:
(def mod1 (require "./localfile"))
(defn main [& args]
(put module/cache "mod1" mod1)
# now calls to (import1 mod1) at runtime will succeed.
(import mod1))
Working Directory Imports
Starting in 1.14.1, Janet will allow imports relative to the current working directory.
This is very useful in scripting circumstances where source code must be loaded at runtime.
This can be done with the function dofile
, but it can also be done with the default
import system.
To import a file relative to the working directory, prefix the import path with a forward slash "/".
(import /local)
This will import from a file ./local.janet
, (or ./local.so
, ./local.jimage
, and so on), and
prefix the imported symbols with local/
.
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 via defn
, macros defined defmacro
, constants defined via
def
, and vars defined via var
will be exported in your module. To
mark a function or binding as private to your module, you may use defn-
or def-
at the top level. You can also add the :private
metadata
to the binding.
Sample module:
# 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 stored on disk at mymod.janet
,
we 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