Skip to main content

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:

  1. Compile the module until all import statements are found.
  2. Process all imported second level modules, repeating from step 1.
  3. If a circular dependency was found, raise a compile time error.
  4. Replace all import statements with the loaded values.
  5. 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 that using ! is not allowed since there is no implicit using *.
// 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 and t.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 and t.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:

  1. If the imported module defines the type that appears in the using list, then the provisions of that type are imported.
  2. 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.