Learn · Chapter 11 of 12

Modules and visibility

Splitting a program across files. import brings names in, pub decides which names leave, CAPA_PATH tells the loader where to look beyond your project directory.

Two files

Put a helper in util.capa:

// util.capa
pub fun greet(name: String) -> String
    return "Hello, ${name}"

Use it from main.capa in the same directory:

// main.capa
import util

fun main(stdio: Stdio)
    stdio.println(greet("Capa"))

Run it:

$ capa --run main.capa
Hello, Capa

Three things to notice. The import util resolves to ./util.capa: paths are relative to the file doing the import. The pub keyword in util.capa marked greet as public; without it, the call from main.capa would fail. And the imported function is reachable directly by its unqualified name (greet(...)) without prefixing the module.

Private by default

Drop pub and see what happens:

// util.capa
fun greet(name: String) -> String
    return "Hello, ${name}"
$ capa --run main.capa
main.capa:3:19: error: undefined name 'greet'
                       (private to module 'util'; mark it 'pub' to expose)
   3 |     stdio.println(greet("Capa"))
                         ^

Every top-level item is private by default. Only items marked pub are reachable from other modules. The diagnostic tells you exactly what to do: add pub to the declaration you wanted to expose.

This is the opposite of Python (where everything in a module is importable unless underscore-prefixed). The Capa default forces you to think about your module's surface: the names you mark pub are the contract; the names you do not are implementation detail you can refactor freely.

Subdirectories: dotted imports

A larger project can nest files in folders. import foo.bar resolves to ./foo/bar.capa:

my-project/
    main.capa
    util/
        strings.capa
        math.capa
// main.capa
import util.strings
import util.math

fun main(stdio: Stdio)
    stdio.println(capitalise("capa"))   // from util/strings.capa
    stdio.println("${double(7)}")        // from util/math.capa

One import per file, no globs. Transitive imports work: main.capa imports util.strings, which itself imports util.math, and the loader follows the chain. Cycles are detected (a.capa imports b.capa which imports a.capa) and surfaced as a precise error.

Qualified access and aliases

Sometimes two modules export the same name. Or you want to make the source of a function explicit at the call site. Qualified access does both:

import util

fun main(stdio: Stdio)
    stdio.println(util.greet("Capa"))

The util.greet form is equivalent to the unqualified greet, except the source is named at the call site. Both work side by side.

To resolve a name conflict between two modules, alias one of them:

import util.strings as S
import third_party as TP

fun main(stdio: Stdio)
    stdio.println(S.capitalise("capa"))
    stdio.println(TP.capitalise("capa"))

The default alias when you do not write as is the last segment of the dotted path: import util.strings aliases to strings, so strings.capitalise(...) works too.

CAPA_PATH for shared modules

The loader resolves import foo relative to the importing file first. If you have a directory of modules you want to share across projects (a personal stdlib, in effect), point CAPA_PATH at it:

$ export CAPA_PATH=/usr/local/share/capa:./vendor
$ capa --run app.capa
# `import greeter` now resolves to ./greeter.capa first,
# then ./vendor/greeter.capa, then /usr/local/share/capa/greeter.capa.

Project-local files always shadow CAPA_PATH entries: if both exist, the local wins. The variable is colon-separated on Unix, semicolon on Windows (os.pathsep). Missing or non-existent directories on the path are silently skipped, so a stale CAPA_PATH does not break every run.

There is no package manager yet. CAPA_PATH is the v1 way to share modules across projects.

The two error modes

First, the one we already met: trying to call a private function from outside its module.

error: undefined name 'helper'
       (private to module 'util'; mark it 'pub' to expose)

The diagnostic is specific to the cause: an unresolved name that happens to be a private of an imported module gets the "private to module X" hint instead of the typo guess.

Second: a name conflict between two imports.

import a    // exports `helper`
import b    // also exports `helper`
error: name conflict: 'helper' declared in
       a.capa (line 1) and b.capa (line 1).
       Either rename one or pick the import explicitly.

Two ways out. Rename one of the two functions, or alias one of the imports (import a as A) and access the conflicting name qualified (A.helper(...)).

Try this. Split chapter 10's email example across files. mailer.capa exports pub capability SendEmail and pub fun make_mailer(net, server). main.capa imports it, builds a mailer, and calls a function from a third file users.capa that takes the SendEmail as a parameter and sends a notification.

Where you are now

You can split your code across files, control what each module exposes, and resolve dependencies the loader knows nothing about by pointing CAPA_PATH at them. With the building blocks of the previous ten chapters, you have everything you need to build a small but real program.

The final chapter does that. A small CLI tool that does something useful, written end-to-end in Capa, using most of what you have learned.