Packages & Modules
So far, we’ve focused on the contents of one module file. That’s a good place to start, but real Par programs live in packages, and packages are made of modules.
The hierarchy looks like this:
- A program or a library is a package.
- A package contains modules.
- A module contains type definitions, declarations, and definitions.
Let’s now look at how those pieces fit together.
Packages
A Par package is a project directory with a Par.toml file and a src/ directory.
For example:
hello_par/
Par.toml
src/
Main.par
This is exactly what par new hello_par creates.
The Par.toml file starts like this:
[package]
name = "hello_par"
This name is the package’s recommended name. It is used by tooling such as generated
docs.
Dependencies
Packages may depend on other packages through the [dependencies] section:
[package]
name = "postify"
[dependencies]
web = "github.com/author/par-web"
shared = "../shared"
Each dependency has the form:
alias = "reference"
The alias on the left is the name used in imports, such as @web/....
The reference on the right may be one of two things:
- A local path. These are recognized by starting with
.,..,~, or$. - A remote dependency source. Anything else is treated as remote, for example
github.com/faiface/par-cancellable.
Local paths may look like:
shared = "./shared"
common = "../common"
tools = "~/par/tools"
extras = "$PAR_PACKAGES/extras"
Remote dependencies currently have no versioning. Par just fetches the latest contents from the given source.
Managing remote dependencies
Remote dependencies are managed by the CLI:
par addreadsPar.tomland fetches any missing remote dependencies intodependencies/.par updatere-fetches all managed remote dependencies.par add github.com/faiface/par-cancellableboth adds that dependency toPar.tomlunder its recommended name and fetches it.
The dependencies/ directory is managed state. It is not where you write source code.
Transitive remote dependencies are fetched automatically. If the same remote package is reached through multiple dependencies, Par handles that seamlessly and fetches it only once.
Local dependencies are not copied into dependencies/. They stay where they are on disk and are
referenced directly.
Built-in packages
Every package automatically depends on two built-in packages:
@corefor core types and data structures such asString,List,Map,Result, and so on.@basicfor simple I/O such asConsole,Os, andHttp.
These are implicit dependencies, but not implicit imports. Their modules are available to import from, but their names are not automatically in scope inside your source files.
Browsing packages with par doc
The par doc command is all about exploring packages:
- Outside any package,
par docshows the built-in packages. - Inside a package,
par docshows the current package together with its dependencies. par doc --remote github.com/faiface/par-cancellablelets you inspect a remote package without manually adding it as a dependency.
Modules
Modules live under src/, in any directory structure you like.
For example:
src/
Main.par
data/
Post.par
handlers/
api/
Posts.par
Here:
src/Main.pardefines theMainmodule.src/data/Post.pardefines thePostmodule with the pathdata/Post.src/handlers/api/Posts.pardefines thePostsmodule with the pathhandlers/api/Posts.
Notice the split:
- A module has a name such as
Post. - A module also has a path such as
data/Post.
The name is what appears in the file:
module Post
The path is how the module is imported from elsewhere. The file name and the module declaration must match, case-insensitively.
So:
Post.parmust declaremodule Postpost.parmay also declaremodule Posthandlers/api/Posts.parmust declaremodule Posts
The directories contribute to the module’s path, not to its declared module name.
Importing modules
Modules import other modules explicitly.
To import a module from the same package, use its absolute path from src/:
import data/Post
import handlers/api/Posts
Relative module imports are not supported.
Import paths always use forward slashes:
import util/DateTimeUtils
To import a module from a dependency, prefix the path with @alias:
import @web/http/Server
import @shared/FancyModule
Aliasing imports
If two modules would clash, rename them on import:
import @dep1/blah/Data as Data1
import @dep2/bleh/Data as Data2
Grouped imports
Multiple imports can be grouped:
import {
@basic/Console
@core/List
data/Post
}
Grouped imports are just syntax sugar over multiple import statements.
Accessing names from imported modules
Once a module is imported, its exported items are accessed through the module name:
import {
@core/List
@core/Nat
@core/String
data/Post
}
dec RenderPosts : String
def RenderPosts = Post.FetchAllFromDB(!)->List.Length->Nat.ToString
In general, imported names are accessed as:
Module.Name
Primary types and primary declarations
A module may export a type and/or a declaration with the same name as the module itself.
Those are special: they become available directly under the module name.
For example:
module Post
export {
type Post = box choice {
.title => String,
.content => String,
}
dec Post : [String, String] Post
dec FetchAllFromDB : [!] List<Post>
}
Now another module can write:
import data/Post
and gets access to:
Postas a typePostas a declarationPost.FetchAllFromDBas another exported declaration from the module
This works smoothly with aliases too:
import @core/List as L
After that:
L<a>is the primary type of the moduleL.Map,L.Filter, and so on are other exported declarations
Having both a primary type Post and a primary declaration Post is perfectly fine,
because in Par, types and terms are always distinguishable by syntactic position.
Visibility and exports
There are two related visibility questions:
- Is the module visible outside its package?
- Is a type or declaration visible outside its module?
Exporting a module
Modules are visible inside their own package regardless of export module.
To make a module visible to dependent packages, mark it:
export module List
Exporting items
Types and declarations are module-private by default.
To make them visible outside the module, use export:
export type Iterator<a> = ...
export dec Map : ...
Or grouped:
export {
type Iterator<a> = ...
dec Map : ...
dec Filter : ...
}
There is no export def.
Definitions are always the implementation side. If you want a value to be visible, export its declaration.
The three visibility levels
Putting the two layers together gives three effective visibility modes for items:
export module+ exported item
The item is visible to dependent packages.- Non-exported module + exported item
The item is visible throughout its own package, but not outside it. - Any module + non-exported item
The item is visible only inside its own module.
Visibility is checked through types too
Par also checks that visible API does not mention hidden types.
For example, if a declaration is visible throughout the package, or exported from the package, then its type must not mention a less-visible type. In other words:
- a package-visible item may not mention a module-private type
- a public item may not mention a merely package-visible type
This prevents less-visible helper types from leaking into wider APIs.
Cycles
In the previous section, we already saw that types and definitions may not use one another in a cycle, and package dependencies follow the same spirit: cyclic dependencies between packages are disallowed.
Modules are different, though: cyclic imports between modules of the same package are allowed.
Why is that useful, if definitions still may not form cycles? Because modules often need one another’s types in their signatures, and some definitions in one module may legitimately call definitions in the other as long as no actual usage cycle is formed.
For example, the built-in @core/List module imports @core/Nat because List.Length returns a
Nat. At the same time, @core/Nat imports @core/List because Nat.Range returns a
List<Nat>.
So the modules import each other, but there is still no cyclic usage between the individual definitions themselves.
Multi-file modules
If one file becomes too large, a module may be split across several files in the same directory:
src/
Parser.par
Parser.lexing.par
Parser.errors.par
These all belong to the same module:
module Parser
The rules are:
Parser.parand everyParser.*.parin the same directory are parts of one module.- Every part still declares
module Parser. - All parts share one top-level module namespace.
- Each file has its own imports that apply only within that file.
- All parts must agree on whether the module is marked
export module.
Running a definition
par run is specifically for definitions of type ! — the unit type, comparable to null or an
empty tuple in other languages.
Other non-generic definitions can still be run in the playground, which generates an automatic UI for interacting with them based on their type.
Now that modules have paths, the par run target syntax makes more sense:
- Targets have one of these forms:
path/to/Modulepath/to/Module.Def
- The slash-separated part is always the module path.
- If the target ends right after the module path,
par runlooks for the module’sMaindefinition. - If the target ends with
.Def, Par runs that specific definition from the module.
So:
par runmeansMain.Mainpar run Mainalso meansMain.Mainpar run Main.Othermeans theOtherdefinition in theMainmodulepar run handlers/api/Postsmeanshandlers/api/Posts.Mainpar run handlers/api/Posts.Programmeans theProgramdefinition in thehandlers/api/Postsmodule
The same path-based rules are used by other commands such as par test.
That’s the package/module system. With that in place, we can now return to the language itself.