Project & Modules
So far, every example has been a single-file script with a main function.
Modules let you split code across files with namespaces and visibility control.
Creating a Project
To use modules, you need a project. Create one with:
saga new my-appThis creates a directory with a project.toml and a Main.saga file. From
here you can add more .saga files, each declaring its own module.
Declaring a Module
A module file declares its name at the top:
module MathModule names do not need to match the file path. A file at
lib/utils/helpers.saga can declare module Helpers or module Utils.Helpers
or anything else. The compiler discovers all .saga files in your project and
resolves imports by declared module name.
Nested modules use dots:
module Data.CollectionsScript files (single files with just a main function) do not declare a module
name. Modules are only used in projects.
Imports
Import a module to use its public definitions:
# Import a module (access via Math.abs, Math.max)
import Math
# Import with an alias (access via M.abs, M.max)
import Math as M
# Import specific names into scope (access as bare abs, max)
import Math (abs, max)
# Both alias and specific names
import Math as M (abs, max)A plain import Math gives you qualified access only: Math.abs, Math.max.
To use names without a qualifier, import them explicitly with
import Math (abs, max).
When two modules export the same name, use qualifiers to disambiguate:
import Data.List
import Data.Set
let xs = List.map f items
let ys = Set.map f itemsAliases let you choose a different qualifier:
import Data.List as L
L.map f itemsVisibility
By default, definitions are private to their module. Use pub to export them:
pub fun add : Int -> Int -> Int # function
pub type Shape = Circle Float | Rect Float Float # ADT
pub record User { name: String, age: Int } # record
pub handler console for Log { ... } # handlerPublic functions require a type annotation. This means making something public forces you to document its type, which serves as both a stable API contract and inline documentation for anyone reading the module's exports. Private functions can be fully inferred:
# Public: annotation required
pub fun abs : Int -> Int
abs n when n < 0 = -n
abs n = n
# Private: annotation optional, type is inferred
double x = x * 2Opaque Types
An opaque type exports the type name but hides its constructors. Other modules can pattern match on it but cannot construct values directly:
module Auth
opaque type Token = Token String
pub fun make_token : String -> Token
make_token secret = Token (hash secret)module App
import Auth (Token, make_token)
# Pattern matching is fine
case token {
Token _ -> "has a token"
}
# Type error: Token constructor is not visible outside Auth
let bad = Token "forged"This enforces invariants: values of the type can only be created through the module's public API.
Project Structure
A Saga project uses a project.toml file at its root. The simplest form
declares an executable:
[project]
name = "my-app"
[bin]
main = "src/Main.saga"[bin].main defaults to Main.saga. The main file must define a main
function.
The compiler scans all .saga files in the project directory to build the
module map. Running saga build, saga run, or saga test starts from the
project root (the directory containing project.toml).
Test files live in a tests/ directory by default and are discovered
automatically by saga test.
Libraries
A project can be a library that other projects depend on. Add a [library]
section to declare what's exposed:
[project]
name = "mathlib"
[library]
module = "Math" # root namespace, required
expose = ["Math", "Math.Vector", "Math.Matrix"] # public modules, required[library].moduleis the root namespace. Every module listed inexposemust be prefixed by it.[library].exposelists the modules consumers canimport. Unlisted modules are still compiled (they're needed at runtime) but are invisible to the type system, so consumers can't import them. This lets you keep internal helpers private while still using them inside the library.
A project can have [library], [bin], or both. A combined project ships an
executable while also being usable as a dependency:
[project]
name = "my-app"
[library]
module = "MyApp"
expose = ["MyApp.Client", "MyApp.Types"]
[bin]
main = "Main.saga"For consuming dependencies (Hex, git, path), see Ecosystem. For calling Erlang or Elixir libraries directly, see Interop.