Lily's predefined symbols are all available in the prelude
module. It is the
only module that has all symbols available at all times, and which cannot be
imported. All other modules operate in a scope by themselves. Symbols from one
module are not available to other modules, unless imported. Import can either
name specific symbols, or opt to use the module as a namespace.
Lily allows modules to have code outside of a definition. If toplevel code exists, it is run the first the module is imported.
# file: fibmodule.lily
define fib(n: Integer): Integer
{
if n < 2: {
return n
else:
return fib(n - 1) + fib(n - 2)
}
}
# file: other.lily
import fibmodule
# Invalid because fib is not in this scope.
# print(fib(10))
# Invalid because fibmodule is not a value.
# print(fibmodule)
define fib(n: Integer): Integer
{
return fibmodule.fib(n)
}
print(fib(10)) # 55
By importing fibmodule
, other
is able to access all symbols inside of
fibmodule
by using fibmodule
as a namespace.
Lily's import may look like Python's at first glance, but there are some important distinctions.
The Lily interpreter is designed to be embedded in other programs. Embedders are
able to register a module for use by scripts they run. One example of that is
FascinatedBox/lily-apache, which
makes a server
module available to scripts it runs.
Another important distinction is the dynaload mechanism in the interpreter. When
a foreign module such as sys
is imported, the interpreter does not
automatically load all symbols inside. Instead, it transparently loads symbols
as they are referenced in the source module. If import sys
is run, sys.argv
is not loaded until sys.argv
is seen. As a result, the interpreter does not
export modules as values.
The most important different is how Lily selects paths when attempting to do importing.
This example farm
program should help to illustrate why Lily's import
mechanism works the way it does.
In the beginning, there is only the farm.
farm.lily
This farm is empty. It needs a barn for storage, and workers.
barn.lily
farm.lily
farmer.lily
Right now, farm can import farmer
and import barn
, and all works well. But
these files need to be contained somewhere. After some reorganization, this
emerges.
farm/
src/
barn.lily
farm.lily
farmer.lily
What's a farm without a field of items to grow?
farm/
src/
farm.lily
farmer.lily
barn.lily
field/
beans.lily
brussel sprouts.lily
cauliflower.lily
peas.lily
Might as well place workers in their own space too.
farm/
src/
barn.lily
farm.lily
field/
beans.lily
brussel sprouts.lily
cauliflower.lily
peas.lily
worker/
farmer.lily
Suppose the farmer wants to import food from the field to use it. How should the farmer refer to it?
Running the farm starts from farm.lily
and spreads out from there. Lily solves
this question by saying that the first file loaded is a root module. When
loading modules, paths need to be specified relative to a root module.
# file: farmer.lily
# This becomes "beans".
import "field/beans"
# By default, this does not get an identifier.
# A name can be given as the identifier, but is not required.
import "field/brussel sprouts" as sprouts
# file: beans.lily
import "field/peas"
Even though beans.lily
is in the same directory as peas.lily
, it must still
pass field/peas
instead of only peas
to import.
Farms often have a tractor that's made somewhere else. Assume files for a tractor were copied in as follows.
farm/
src/
barn.lily
farm.lily
field/
beans.lily
brussel sprouts.lily
cauliflower.lily
peas.lily
tractor/
base_tractor.lily
tractor.lily
wheel.lily
worker/
farmer.lily
This does not work!
Because the tractor's files were copied in, they assume that the tractor is the
root module. Since barn.lily
is the root, they no longer work.
Similarly, if the files from farm
were copied into another project, they would
fail for the same reason.
Note how farm
begins at farm/src/farm.lily
. Suppose tractor
could do the
same. A near-final version of the farm
looks like this.
farm/
src/
barn.lily
farm.lily
field/
beans.lily
brussel sprouts.lily
cauliflower.lily
peas.lily
packages/
tractor/
src/
tractor.lily
worker/
farmer.lily
When the farmhand farmer.lily
asks to import tractor
, Lily uses the second
strategy of looking at packages/<name>/src/<name>
relative to farm.lily
.
When Lily imports a module through a packages directory, it marks the module it
sees as a root module. Modules inside tractor
will be relative to tractor
,
and farm
relative to farm
.
What if the tractor does not want to export all details to the farm? A final example is as follows.
farm/
src/
barn.lily
farm.lily
field/
beans.lily
brussel sprouts.lily
cauliflower.lily
peas.lily
packages/
tractor/
src/
base_tractor.lily
tractor.lily
run_tractor.lily
wheel.lily
worker/
farmer.lily
base_tractor.lily
defines all the symbols that the tractor needs. It imports
wheel through import wheel
, as well as any other files it may need.
tractor.lily
imports the parts of base_tractor.lily
that are meant to be
visible to the public.
run_tractor.lily
is a file to execute to run the tractor directly.
wheel.lily
is a file imported by base_tractor.lily
.
Because of how Lily's importing works, the tractor package can assume that only
tractor.lily
will be imported by the outside.
In short, importing in Lily is relative to a root module. Given an import
of farm
and search for barn
, it will search as follows.
barn.lily
barn.{dll/dylib/so}
packages/barn/src/barn.lily
packages/barn/src/barn.{dll/dylib/so}
By default, a module's name is the filename minus the suffix. It is possible to give the interpreter a different name to use instead.
This cannot be mixed with direct imports (import (<symbols>) <target>
).
# file: fibmodule.lily
define fib(n: Integer): Integer
{
if n < 2: {
return n
else:
return fib(n - 1) + fib(n - 2)
}
}
# file: other.lily
import fibmodule as newfib
print(newfib.fib(10)) # 55
It is also possible to import multiple modules at once. This example imports multiple predefined modules. Each entry in a multi import can execute any import features mentioned here.
# file: multi.lily
import introspect, sys, time
print(time.Time.now()) # <Time at 0x--->
It is also possible to import symbols directly from other modules. Any kind of symbol except a module can be directly imported from a module.
This cannot be mixed with redeclaration (import <target> as <name>
).
Symbols in the prelude
module are
available to all modules without importing. For all other modules, the
symbol must be imported, or the module imported and used as a namespace.
For one module to access symbols from another module, it has to import the
symbols directly (import (<symbol>) module
), or import the module and access
the symbols with the module as a namespace (import module
).
# file: direct.lily
import (argv) sys,
(Time) time
print(Time.now()) # <Time at 0x--->
print(argv) # []
It is intentionally not possible to import all symbols from a module. By requiring all symbols to be explicitly named, it is easier to know where all symbols in a file originate from.
Lily imports always use the forward slash (/
) as a directory separator. On
Windows, the forward slash is translated to a backslash. It is a syntax error to
use a backslash.
If a path is given that has slashes, the default module name will be the
filename. In the below example, somedir/point
becomes point
.
# file: somedir/point.lily
class Point(public var @x: Integer, public var @y: Integer)
{
public define stringified: String {
return "Point({}, {})".format(@x, @y)
}
}
# file: first.lily
import "somedir/point"
print(point.Point(10, 20).stringified()) # Point(10, 20)