Modules
A module is a file of Fuyu source text that declares bindings which can be loaded into another module. Module-level declarations can appear in order. This means that module-level types, values, and functions can be used before they are declared.
import std/io
def main() {
io::println(greeting)
}
let greeting = "Hello!"
Module-level bindings must not have circular dependencies. The compiler will determine which order they should be evaluated.
let y = 2 * x + 1 // y is evaluated to 7.
let x = 3
Module-level declarations must form an acyclic dependency graph. This can be summarized by the following rules:
- Values declared with
let
cannot be part of a dependency cycles. - Functions declared with
def
can form dependency cycles with other functions. - Types declared with
type
can form dependency cycles with other types.
When a module defines a main
function, it can be evaluated as a program.
After all module-level declarations are evaluated, the main
function is
evaluated.
import std/io
def main() {
io::println("hello")
}
Imports
Everything that is exported from a module with export
can be imported by
another module.
// Filename: my_module.fuyu
export let magic_number = 1234
let greeting: String = "Hello"
export def greet(name: String) -> String {
greeting + ", " + name + "!"
}
When imported into another module, the magic_number
value and greet
function are made available, but the greeting
value is not.
// Filename: main.fuyu
import std/io
import my_module
def main() {
io::println(mymodule::greet("Samantha"))
assert_eq(mymodule::magic_number, 1234)
}
By default, an import statement imports the specified module, creating a namespace with the name of the path stem (i.e., the final component) and includes all exported provisions.
import a/b/mod
This import does the following:
- Searches the import path for
a/b/mod.fuyu
and imports it. - Creates a namespace of
mod
through which the contents of the module can be accessed with::
. - Imports all exported provisions in
mod
for resolution by the compiler. These provisions cannot be explicitly named by the programmer.
Specific names can be loaded using an import list in (...)
. This hides the
module namespace unless it is included in the import list with the self
keyword. Additionally, the *
can be used to mean all exported names in the
module.
// Imported entities shown in the trailing comment.
import a/b/mod::(x, T) // `x`, `T`, all provisions.
import a/b/mod::(self, x, T) // `mod`, `x`, `T`, all provisions.
import a/b/mod::(*) // All names, all provisions.
import a/b/mod::(self, *) // `mod`, all names, all provisions.
To control which provisions are imported, a using
list can be supplied with
(using ...)
to provide either type-directed or name-directed resolution.
Unlike a import list, a using
list does not hide the module namespace. To
completely hide provisions, a using
list of !
can be given. A using
list
can often be elided, as the default behavior is using *
, which imports all
exported provisions.
// Imported entities shown in the trailing comment.
import a/b/mod // `mod`, all provisions.
import a/b/mod::(using *) // `mod`, all provisions.
import a/b/mod::(using ctx) // `mod`, provision named `ctx`.
import a/b/mod::(using Add[_]) // `mod`, provisions unifying with `Add[_]`.
import a/b/mod::(using Add) // `mod`, provisions unifying with `Add`.
import a/b/mod::(using Add[Int]) // `mod`, provisions unifying with `Add[Int]`.
import a/b/mod::(using ctx, Add) // `mod`, provision named `ctx`,
// and provisions unifying with `Add`.
import a/b/mod(using !) // `mod` (no provisions).
Anything except type-directed provisions can be renamed using as
. The
original name is not imported, and the entity is only available under the new
name. Type-directed provisions cannot be renamed because it is possible for
more than one value to unify with the type.
// - Imports the module namespace as `m`.
// - Imports a value named `x` accessible only through the name `y`.
// - Imports a type or type constructor named `T` accessible only through the name `U`.
// - Imports a provision named `ctx` accessible only through the name `c`.
// - Imports all provisions unifying with `Add`.
import a/b/mod::(self as m, x as y, T as U)(using ctx as c, Add)
Sub-namespaces are not loaded automatically.
import std
// Uncommenting this is an error, only direct descendents of std can be used.
//std::io::println("hello")
// This works!
import std/io
io::println("hello")
An import
can be located in the module-level scope or in a tighter local
scope such as a function body.
// The io namespace is in scope for the entire module.
import std/io
def say_hello() {
// The fmt namespace is scoped to only in the say_hello() function.
import std/fmt
io::println(fmt::debug("hello"))
}
Circular import dependencies are not possible as the compiler needs to evaluate a module before it can be used. When a module is loaded, the compiler performs the following steps:
- Compile the module until all
import
statements are found. - Process all imported second level modules, repeating from step 1.
- If a circular dependency was found, raise a compile time error.
- Replace all
import
statements with the loaded values. - Evaluate the module.
A module import is cached, so if a module is loaded multiple times it is coherent and the same copy is used for lifetime of the program.
Exports
Exports can be used in two ways. The first is to export a type or binding.
// All of these can be imported by another module.
export type Count(int)
export let zero = Count(0)
export def increment(count: Count) -> Count {
Count(count.0 + 1)
}
The second is an export
statement, that is the dual of an import
statement.
The export statement allows exports from a module dependency to be re-exported.
Like import
statements, export
statements can specify export and using
lists, but the semantics are a little different.
- Nothing is automatically re-exported from an
export
statement. - Either an export list,
using
list, or both must be specified. - Export and using lists follow the same rules as
import
expressions; however, the only difference is thatusing !
is not allowed since there is no implicitusing *
.
// Some export statement examples.
export a/b/c::(*) // Re-export all bindings.
export a/b/c::(*)(using *) // Re-export all bindings and provisions.
export a/b/c::(using *) // Re-export all provisions.
export a/b/c::(x)(using ctx, Add) // Re-export a binding named `x`,
// a provision named `ctx`,
// and all provisions unifying with `Add`.
An export
statement does not import anything into the module in which it
appears, and an explicit import is required to bring those names and provisions
into scope.
// value.fuyu
export let x = 10
// main.fuyu
export value::(x)
// Uncommenting the following line is an error since x is exported only.
// let y = x + 1
Transparency
The export
keyword describes whether a binding is exported when a module is
loaded. The transparent
keyword augments export
types by stating that the
internal structure of the type is also exported.
For algebraic types, transparent
exports all constructors.
export type Ab { // Exported.
A // Not exported.
B // Not exported.
}
export transparent type Cd { // Exported.
C // Exported.
D // Exported.
}
If the algebraic constructors take arguments, then those arguments are exported and follow the same tuple-like and struct-like constructor rules below.
export type OpaqueTuple[a, b](a, b)
export transparent type TransTuple[a, b](a, b)
export type OpaqueRecord[a, b](left: a, right: b)
export transparent type TransRecord[a, b](left: a, right: b)
OpaqueTuple
: Only the name of the type is exported.TransTuple
: The type name, constructor, and fields are exported. Values can be constructed (e.g.let t = TransTuple(3, 4)
) and fields accessed (e.g.t.0
andt.1
).OpaqueRecord
: Only the name of the type is exported.TransRecord
: The type name, constructor, and fields are exported. Values can be constructed (e.g.let t = TransRecord(left: 3, right: 4)
) and fields accessed (e.g.t.left
andt.right
).
Transparency is all or none. Either all of the type details are made transparent or none of them. Types that are not transparent are called opaque, since no internal details are known about them.
Private modules within a package
Modules can be made private to a package by prefixing the module name with _
.
When a file or directory name begins with _
, it can only be imported by
modules located in the same directory. The module search path will not be
followed for module paths beginning with _
. Consider the following directory
structure.
src/
| main.fuyu
| _a.fuyu
| _b/
| c.fuyu
| _d.fuyu
Within main.fuyu
, there are the following behaviors.
// main.fuyu
import _a // Allowed.
import _b/c // Allowed.
// import _b/_d // Not allowed.
The import of _b/_d
is not allowed, because _d.fuyu
is not in the same
directory as main.fuyu
. The import can only be resolved if the first path
component begins with _
, as is the case for _a
and _b/c
.
Using list disambiguation
Type directed imports in a using list can be ambiguous.
// a.fuyu
export transparent type Value(Int)
// b.fuyu
import a
type Value(Int)
export provide Value(123)
export provide a::Value(123)
// main.fuyu
import a::(Value)
import b::(using Value) // a::Value(123) or b::Value(123)?
To solve this problem, resolution uses the following order of priority for using lists, stopping at the first that is satisfied:
- If the imported module defines the type that appears in the using list, then the provisions of that type are imported.
- If the type in the using list is in scope at the site of the import statement, then the provisions of that type are imported.
Explicit disambiguation is achieved using self::Value
or a::Value
in the
using list.