Modules

Flix supports hierarchical modules as known from many other programming languages.

Declaring and Using Modules

We declare modules using the mod keyword followed by the namespace and name of the module.

For example, we can declare a module:

mod Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
}

Here we have declared a module called Math with a function called sum inside it. We can refer to the sum function, from outside of its module, using its fully-qualified name:

def main(): Unit \ IO = 
    let result = Math.sum(123, 456);
    println(result)

Alternatively, we can bring the sum function into local scope with use:

def main(): Unit \ IO = 
    use Math.sum;
    let result = sum(123, 456);
    println(result)

Using Multiple Declarations from a Module

If we have multiple declarations in a module:

mod Math {
    pub def sum(x: Int32, y: Int32): Int32 = x + y
    pub def mul(x: Int32, y: Int32): Int32 = x * y
}

We can, of course, use each declaration:

use Math.sum;
use Math.mul;

def main(): Unit \ IO =
    mul(42, 84) |> sum(21) |> println

but a shorter way is to group the uses together into one:

use Math.{sum, mul};

def main(): Unit \ IO =
    mul(42, 84) |> sum(21) |> println

Note: Flix does not support wildcard uses since they can lead to subtle bugs.

Avoiding Name Clashes with Renaming

We can use renaming to avoid name clashes between identically named declarations.

For example, if we have two modules:

mod A {
    pub def concat(x: String, y: String): String = x + y
}

mod B {
    pub def concat(xs: List[Int32], ys: List[Int32]): List[Int32] = xs ::: ys
}

We can then use each concat function under a unique name. For example:

use A.{concat => concatStrings}
use B.{concat => concatLists}

def main(): Unit \ IO =
    concatStrings("Hello", " World!") |> println

While this feature is powerful, in many cases using a fully-qualified might be more appropriate.

Modules and Enums

We can define an enum inside a module. For example:

mod Zoo {
    pub enum Animal {
        case Cat,
        case Dog,
        case Fox
    }
}

Here the Zoo module contains an enum type named Animal which has three cases: Cat, Dog, and Fox.

We can access the type and the cases using their fully-qualified names:

def says(a: Zoo.Animal): String = match a {
    case Zoo.Animal.Cat => "Meow"
    case Zoo.Animal.Dog => "Woof"
    case Zoo.Animal.Fox => "Roar"
}

def main(): Unit \ IO = 
    println("A cat says ${says(Zoo.Animal.Cat)}!")

Alternatively, we can use both the Animal type and its cases:

use Zoo.Animal
use Zoo.Animal.Cat
use Zoo.Animal.Dog
use Zoo.Animal.Fox

def says(a: Animal): String = match a {
    case Animal.Cat => "Meow"
    case Animal.Dog => "Woof"
    case Animal.Fox => "Roar"
}

def main(): Unit \ IO = 
    println("A cat says ${says(Cat)}!")

Note that use Zoo.Animal brings the Animal type into scope, whereas use Zoo.Animal.Cat brings the Cat case into scope.

Modules and Trait

We can also define a trait inside a module. The mechanism is similar to enums inside modules.

For example, we can write:

mod Zoo {
    pub trait Speakable[t] {
        pub def say(x: t): String
    }
}

enum Animal with ToString {
    case Cat,
    case Dog,
    case Fox
}

instance Zoo.Speakable[Animal] {
    pub def say(a: Animal): String = match a {
        case Cat => "Meow"
        case Dog => "Woof"
        case Fox => "Roar"
    }
}

We can use fully-qualified names to write:

def speak(x: t): Unit \ IO with Zoo.Speakable[t], ToString[t] = 
    println("A ${x} says ${Zoo.Speakable.say(x)}!")

def main(): Unit \ IO = 
    speak(Animal.Cat)

Or we can use the Zoo.Speakable trait and the Zoo.Speakable.say function:

use Zoo.Speakable
use Zoo.Speakable.say

def speak(x: t): Unit \ IO with Speakable[t], ToString[t] = 
    println("A ${x} says ${say(x)}!")