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 name 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)}!")