Skip to main content

Modules and packages

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, constants, and functions can be used before they are declared.

import std/io

#[public]
greet: () -> ()
greet () -> io.println greeting

greeting: String
greeting = "Hello!"

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

main: () -> ()
main () = io.println "hello"

Modules in Fuyu are logically consistent with how the source code is stored. Every file is a module, and modules form a tree represented by the structure of files and directories.

Namespaces

A namespace is created when a module is imported. Namespaces are not the same as names; thus, a namespace and a name can be identical, and there is no ambiguity.

value.fuyu
#[public]
value: Int
value = 100
main.fuyu
import std/assert
import ./value // value namespace.

main: () -> ()
main () = do
let value = "hello" // Local value name.
assert.eq value "hello" // Use the local value name.
assert.eq value.value 100 // Use the value namespace.

Packages

NEEDS SPECIFICATION

Packages are part of ongoing design work.

Visibility

Within a module, #[public] attribute modifies the top-level declarations and makes them publicly available to other modules. Declarations without a #[public] attribute are private and can only be used within that module.

physics.fuyu
/// Acceleration due to gravity in m/s^2.
#[public]
g: Float
g = 9.81

// This is not public and cannot be used outside of the module.
/// Vacuum speed of light in m/s.
c: Float
c = 299_792_458.0

/// The famous E = mc² where m is the mass of an object in kg.
#[public]
rest_mass_energy: Float -> Float
rest_mass_energy m = m * c ** 2

This module can be used in another.

import std/fmt
import std/io
import ./physics

/// Mass of an electron in kg.
electron_mass: Float
electron_mass = 9.109_383e-31

main: () -> ()
main () = do
io.println ("g = " + fmt.debug physics.g + " m/s")
let e = physics.rest_mass_energy electron_mass
io.println ("electron rest mass energy is " + fmt.debug e + " J")
// Since `c` is not public in `physics`, this is an error.
// io.println "Speed of light = " + fmt.debug physics.c + " m/s"

Imports

info

Import declarations are only allowed at module level.

By default, an import statement imports the specified module, creating a namespace with the name of the path stem (i.e., the final component). No names are brought into the local namespace, and no implicits are included. This is called a qualified import, because accessing the members of the module must be done by qualifying the module namespace.

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 ..

Specific names can be brought into scope using an import list specified after the for keyword. Note that this still includes the module namespace — the module namespace is always included in an import. This is called an unqualified import, because the members of the imported module are accessed without the namespace.

// Imported entities shown in the trailing comment.
import a/b/mod for type T // `mod`, type `T`.
import a/b/mod for x, T // `mod`, `x`, constructor `T`.

Note that importing type T and T are different. Using type T imports the type, while T on its own is a value constructor.

To control which implicits are imported, they can be qualified by &[...] for either type-directed or name-directed resolution.

// Imported entities shown in the trailing comment.
import a/b/mod // `mod`.
import a/b/mod for &[_] // `mod`, all implicits.
import a/b/mod for &[ctx] // `mod`, implicit named `ctx`.
import a/b/mod for &[Add _] // `mod`, implicits unifying with `Add _`.
import a/b/mod for &[Add] // `mod`, implicits unifying with `Add`.
import a/b/mod for &[Add Int] // `mod`, implicits unifying with `Add Int`.
import a/b/mod for &[ctx], &[Add] // `mod`, implicit named `ctx`,
// and implicits unifying with `Add`.
tip

Importing all implicits with &[_] works because all types unify with _.

Importing a named implicit, such as &[ctx], does not import all implicits. This is because imports are not patterns and instead look for an implicit with the name ctx.

Anything except type-directed implicits can be renamed using as. The original name is not imported, and the entity is only available under the new name. Type-directed implicits 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 value constructor named `T` accessible only through the
// name `U`.
// - Imports an implicit named `ctx` accessible by name only through `c`,
// but is still in the implicit context for the compiler to automatically
// resolve.
// - Imports all implicits unifying with `Add`.
import a/b/mod as m for &[ctx] as c, &[Add], x as y, T as U

Dependency graph

When a module imports another module, it forms a dependency relationship. By connecting all of these dependency relations for a program, a dependency graph can be created.

Consider a program that implements the backend for a webshop. It has an inventory and order log, which are stored in a database.

main.fuyu
import /database
import /inventory
import /order

// Initialize connection to database for orders and inventory...
database.fuyu
// Functions to interact with the database...
inventory.fuyu
import /database

// Functions to manage the inventory...
order.fuyu
import /database

// Functions for processing orders...

Below is a dependency graph for the program, drawn where a -> b means that module a imports module b.

  +-------------------------+
| |
| +--->
main -----> inventory ----------> database
| +--->
| |
+--------> order ---------+

Notice that all arrows point to the right? This is because all dependency relations between modules must form a directed acyclic graph (DAG). In the DAG, if we pick any arbitrary module and follow the direction along the -> arrows, then there is no way to ever reach the starting module, meaning that there are no dependency cycles. In other words, this property means that it is possible to draw the dependency graph such that all arrows point in the same direction.

Fuyu enforces that all modules form a DAG for two reasons:

  1. To simplify compilation, as each module can be compiled in isolation once its dependents have been compiled.
  2. To enable concurrency in the compiler at the module level.

Module search paths

Depending on the path given to import a module, different locations are searched.

There are three flavors of imports:

  1. Package path imports.
    import a/b/c // Search for `a/b/c` in the package path.
    This is how the standard library and external modules are imported.
  2. Relative path imports.
    import ./a/b/c     // Search for `a/b/c` relative to the current module.
    import ../a/b/c // Search for `a/b/c` relative to the parent module.
    import ../../a/b/c // Search for `a/b/c` relative to the grandparent module.
    This is one way that modules within the same package are imported.
  3. Absolute path imports.
    import /a/b/c // Search for `a/b/c` relative to the package root.
    This is one way that modules within the same package are imported.
tip

If an import path starts with an identifier (e.g., std), then it is importing a module outside of the current package.

If an import path starts with punctuation (e.g., /, ./, or ../), then it is importing a module inside of the current package.

Transparency

The #[public] attribute describes whether a declaration is visible when a module is imported. The #[transparent] attribute augments public types by stating that the internal structure of the type is also public.

#[public]
type Ab = // Visible.
A, // Not visible.
B // Not visible.

#[public]
#[transparent]
type Cd = // Visible.
C, // Visible.
D // Visible.

If the constructors take arguments, then those arguments are visible and follow the same tuple struct and record constructor rules below.

#[public]
type Opaque a b = Opaque a b

#[public]
#[transparent]
type Transparent a b = Transparent a b
  • Opaque: Only the name of the type is visible.
  • Transparent: The type name, constructor, and fields are visible. Values can be constructed (e.g., let t = Transparent 3 4) and fields accessed (e.g., t#0, t#1, or deconstruction).

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.

Implicit import disambiguation

Imports of type-directed implicits can be ambiguous.

a.fuyu
// a.fuyu
#[public]
#[transparent]
type Value = Value Int
b.fuyu
import ./a

type Value = Value Int

#[public]
#[implicit]
value: Value
value = 456

#[public]
#[implicit]
a_value: a.Value
a_value = 123
main.fuyu
import ./a for Value
import ./b for &[Value] // `a.Value 123` or `b.Value 456`?

What Value implicit is used in the main.fuyu module? For an unqualified type name (i.e., no namespace), it is always the type that is defined in the module. In the above example, this means that the implicit for b.Value is imported.

If the implicit for a.Value from the b module is to be imported, then it can be disambiguated by including a.Value in the import list.

Leakage of private types

The compiler will prevent leaking private types. Consider the following declaration of the Data type.

// This is module private.
type Data a = Data a

The compiler will disallow the module from making the following public:

  • A binding to a value of a module private type.
    // Illegal.
    // #[public]
    // data: Data String
    // data = Data "hello"
  • A function that accepts or returns a value with a module private type.
    // All of these are illegal.
    // #[public]
    // f: Data a -> ()
    // f _ = ()
    //
    // #[public]
    // g: () -> Data Int
    // g () -> Data 123
  • A transparent type whose constructors include a module private type.
    // This is illegal.
    // #[public]
    // #[transparent]
    // type WrappedData = Wrapped (Data Int)

Note that opaque (i.e., not transparent) types can use module private types in their constructors. For example, the following is legal:

// This is legal because the type is not transparent.
#[public]
type WrappedData = Wrapped (Data Int)