A small project
Putting it together. A grade-book tool that reads student scores from a CSV-ish text file, computes per-student statistics, and writes a small report. Uses most of what the previous eleven chapters introduced.
The brief
The tool reads a file called scores.txt with one student per line in the shape Name,score1,score2,score3. It computes each student's average and pass/fail status (passing is average ≥ 10), then writes a report to report.txt and prints a summary to the terminal.
Input:
Ana,17,15,19
Bruno,8,11,9
Carla,14,13,15
Expected output:
Ana: 17.0 (Pass)
Bruno: 9.33 (Fail)
Carla: 14.0 (Pass)
Passed: 2/3
The shape
Three files. parse.capa turns one input line into a typed record. report.capa takes records and produces the report string. main.capa is the entry point that wires the capabilities.
The data model:
// parse.capa
pub type Student {
name: String,
scores: List<Int>,
average: Float,
passed: Bool
}
pub fun parse_line(line: String) -> Option<Student>
let parts = line.split(",")
if parts.length() < 2
return None
let name = parts.get(0)?
var scores: List<Int> = []
var sum = 0
var i = 1
while i < parts.length()
let token = parts.get(i)?
let n = parse_int(token)?
scores.push(n)
sum = sum + n
i = i + 1
if scores.is_empty()
return None
let avg = to_float(sum) / to_float(scores.length())
return Some(Student {
name: name,
scores: scores,
average: avg,
passed: avg >= 10.0
})
Three things to notice. Each ? hands back None if a parse step fails; the function's return type Option<Student> matches what the analyzer expects (chapter 7). parts.get(i) returns an Option<String> because string-list indexing is total; the ? unwraps. The Student struct holds the original scores plus the derived average and passed, so report generation does not have to recompute.
Report generation
// report.capa
import parse
pub fun render(students: List<Student>) -> String
var out = ""
for s in students
let status = if s.passed then "Pass" else "Fail"
out = out + "${s.name}: ${s.average} (${status})\n"
let passed = students.filter(|s: Student| s.passed).length()
out = out + "\nPassed: ${passed}/${students.length()}\n"
return out
report.capa imports parse so it has the Student type in scope. The function takes a list of students, walks them to build the per-line section, and uses filter().length() for the count of passing students.
The if cond then a else b form is the expression flavour of if from chapter 4; it gives status a value without writing a four-line statement.
The entry point
// main.capa
import parse
import report
fun load_students(fs: Fs, path: String) -> Result<List<Student>, IoError>
let text = fs.read_to_string(path)?
var students: List<Student> = []
for line in text.split("\n")
match parse_line(line)
Some(s) -> students.push(s)
None -> // skip empty / malformed lines silently
return Ok(students)
fun main(fs: Fs, stdio: Stdio)
let work_dir = fs.restrict_to("./")
match load_students(work_dir, "scores.txt")
Err(e) ->
stdio.eprintln("read failed: ${e}")
return
Ok(students) ->
let output = render(students)
match work_dir.write_string("report.txt", output)
Err(e) -> stdio.eprintln("write failed: ${e}")
Ok(_) -> stdio.println(output)
Three things to call out. main takes only Fs and Stdio; the rest of the program is built from those two. load_students is given a narrowed Fs (via fs.restrict_to("./")); even if a future change introduced a path traversal, the narrowed cap would deny it before any filesystem call (chapter 9). And the program propagates failures explicitly through Result with a top-level match; there is no try / except, no panicking, no silent crash.
Run it
$ capa --run main.capa
Ana: 17.0 (Pass)
Bruno: 9.333333333333334 (Fail)
Carla: 14.0 (Pass)
Passed: 2/3
The report.txt file in the working directory contains the same content. Confirm with cat report.txt on Unix or type report.txt on Windows.
The capability manifest for this program tells you what it can do:
$ capa --manifest main.capa | grep declared_capabilities
"declared_capabilities": ["Fs"],
"declared_capabilities": ["Fs", "Stdio"],
"declared_capabilities": [],
"declared_capabilities": []
parse_line and render are listed last: they declare nothing. They are provably incapable of touching the filesystem, the network, or any other system resource. A reviewer looking at the SBOM learns that the parsing and reporting logic is sandboxed from authority by construction, not by convention.
What you have built
A multi-file program that reads a file, parses input into typed records, computes derived values, generates a report, and writes output. Every error path is named in a type. The two functions that do not touch the outside world are provably pure. The two that do, declare exactly what they touch. The narrowing at main's boundary makes the filesystem reach the smallest the program can run with.
This is what writing programs in a capability-typed language looks like in the small. Scale up: more files, more types, more user-defined capabilities for domain operations. The discipline scales with you because the type system enforces it, not the conventions.
Where you go next
The twelve chapters of Learn are now behind you. The language reference and the standard library are the dense companions for lookup. The examples/ directory in the repo has a working program for almost every feature, including the CVE case studies that motivated the design.
If you build something with Capa, send it. The language is in beta; real use is the best feedback.