Modules
Modules let you split a program into smaller named units. In a project,
local modules live under src/, and subdirectories become part of the
module name. For example, src/a/b.act is imported as import a.b
from another module in the same project.
See Projects for how acton build discovers those files
and Package Management for external
dependencies that live alongside local modules.
Package dependencies use the dependency name as an import prefix. If
your Build.act has a dependency named foo, Acton exposes modules
from that dependency under foo:
| Dependency source file | Import path |
|---|---|
foo/src/lib.act | import foo |
foo/src/bar.act | import foo.bar |
foo/src/a/b.act | import foo.a.b |
foo/src/foo.act | import foo.foo |
src/lib.act is the package root module for dependency imports. The
dependency name comes from the key in the consuming project's
dependencies block.
Currently, that dependency name must match the dependency project's
name field.
Use modules to group code by responsibility. A good module has a clear job. For example:
- parsing and validation
- domain logic and shared types
- I/O, network access, or other side effects
- startup and orchestration
Start with one module per clear topic. If a file starts mixing unrelated ideas, move one topic into its own module. Keep the project tree and the module tree aligned so the file layout stays easy to follow.
Import forms
Use import when you want the module name itself:
import timeimport time as timmy
Use from ... import ... when you want specific names directly:
from time import nowfrom time import now as rightnow
import time
import time as timmy
from time import now
from time import now as rightnow
actor main(env):
print(time.now())
print(timmy.now())
print(now())
print(rightnow())
env.exit(0)
Keep imports explicit, and use aliases only when they improve readability or avoid a name clash.
Module-level code
Module-level names are constants. Mutable program state belongs in actors, not in modules.
That means:
- helper functions and constants fit naturally at module level
- mutable variables do not
- program startup logic should live in actors such as
main
default_timeout = 5
def timeout_seconds():
return default_timeout
Modules can also carry docstrings. Because module-level bindings are constant, importing a module is closer to importing a namespace of definitions than shared mutable runtime state.
That is why module boundaries work best when they are stable and purposeful. Prefer names that explain the boundary directly, and keep the module surface small unless the file is intentionally acting as a public API.
How to split code
Split a module when it starts doing too many different jobs.
Good reasons to make a new module include:
- a group of functions is reused from several places
- a feature has a clear boundary, such as
parser,storage, orprotocol - one part of the code changes for different reasons than the rest
- the file has grown enough that imports and names are hard to scan
Avoid creating modules just to make a tree look deep. A short, direct module path is usually easier to work with than a very fine-grained one.