Learn · Chapter 10 of 12

Defining your own capability

Libraries declare their own capability types and the discipline applies uniformly. A function that takes a SendEmail can send mail; nothing else can.

The motivation

The seven built-in capabilities cover IO. They do not cover everything you might want to express as a capability. A library that sends email through SMTP, talks to a database, publishes to a message queue, or signs JWTs has a real authority surface that the type system today only sees as "Net + maybe Fs". From a caller's point of view that is too coarse.

Capa lets a library define a new capability that is just as real as Net or Fs. The discipline applies the same way: any function that wants to send email has to receive a SendEmail value as a parameter.

The declaration

A user-defined capability looks like a trait, but the keyword is capability:

capability SendEmail
    fun send(self,
        to: String,
        subject: String,
        body: String) -> Result<Unit, IoError>

The declaration is the contract. Anyone who implements SendEmail must provide send with this exact signature; anyone who receives a SendEmail value can call send on it without knowing the concrete implementation.

The analyzer treats SendEmail as a capability: it is subject to the same no-aliasing, must-be-used rules as the built-ins.

An implementor

A concrete implementor is a struct paired with an impl SendEmail for ThatStruct:

type SmtpMailer {
    server: String,
    net: Net
}

impl SendEmail for SmtpMailer
    fun send(self,
        to: String,
        subject: String,
        body: String) -> Result<Unit, IoError>
        let _ = self.net.post("smtp://${self.server}/", body)?
        return Ok(())

Two things are unusual about this struct. First, SmtpMailer has a Net field. Built-in capabilities cannot normally appear in struct fields; that would be a way to smuggle authority sideways. The rule relaxes for cap-bearing structs: a struct that implements a user-defined capability is allowed to hold built-in caps as fields, on the condition that it is itself treated as a capability by the discipline.

Second, the constructor is just a regular function. A built-in cap cannot be returned from a function; a user-defined one can. That is the entry point into a user-defined cap: a factory function that takes whatever built-in caps it needs and returns a fresh implementor.

fun make_mailer(net: Net, server: String) -> SmtpMailer
    return SmtpMailer { server: server, net: net }

Inside make_mailer, the built-in net flows into the struct field. After construction, SmtpMailer takes over the discipline: a function that holds a SmtpMailer can send email; one that does not, cannot.

Using it

From a caller's perspective, the user-defined cap behaves exactly like a built-in:

fun notify_user(mailer: SendEmail, user_id: String) -> Result<Unit, IoError>
    mailer.send("${user_id}@example.com", "Welcome", "Hello")?
    return Ok(())

fun main(net: Net, stdio: Stdio)
    let mailer = make_mailer(net, "smtp.example.com")
    match notify_user(mailer, "42")
        Ok(_) -> stdio.println("sent")
        Err(e) -> stdio.eprintln("error: ${e}")

Read the type of notify_user. It declares SendEmail, not Net. Someone reading the signature learns that this function sends email; they do not learn that the underlying transport is HTTP-over-something. The implementation choice is invisible at the call site, which is the point: SendEmail is the contract.

The manifest reflects this. notify_user appears with declared_capabilities: ["SendEmail"], no Net in sight. An auditor reading the SBOM sees the high-level authority surface, not the implementation transport. A reviewer who wants to know what SendEmail ultimately translates to looks at the user_defined_capabilities section of the manifest, which lists every type that implements it.

Why this matters for libraries

Capability-typed libraries are the unique contribution of Capa. In a normal language, a library's authority surface is documented in the README and enforced by nothing. In Capa it is a type, the compiler enforces it, the SBOM emits it.

Library authors get a stable contract: "to use this library you give it a SendEmail; to give it one you decide what implementor to build." Callers get reasoning power: "anything that does not take a SendEmail cannot send mail." Auditors get an SBOM where declared_capabilities is meaningful at the level the user actually cares about.

The pattern scales. A database library exposes capability QueryDB; a message-queue client exposes capability PublishMessage; a JWT signer exposes capability SignToken. None of them appear as built-ins because Capa cannot anticipate every domain; the language makes it cheap for libraries to add their own.

The two error modes

First: a struct that holds a built-in cap but does not implement a user-defined cap.

type Holder {
    net: Net
}
error: type 'Holder' has a capability field 'net' but does not
       implement any user-defined capability; capabilities cannot
       appear as struct fields outside the cap-bearing pattern
   1 | type Holder {
              ^

Either remove the cap field or pair the struct with an impl SomeCap for Holder. The cap-bearing relaxation is opt-in: you have to declare what capability the struct embodies before you are allowed to wrap a built-in inside it.

Second: a method whose signature does not match the trait.

impl SendEmail for SmtpMailer
    fun send(self, to: String) -> Result<Unit, IoError>
        return Ok(())
error: impl SendEmail for SmtpMailer: method 'send' has the wrong
       signature; expected (self, String, String, String) ->
       Result<Unit, IoError>, got (self, String) -> Result<Unit, IoError>

The implementor's method signature must match the capability declaration exactly: same arity, same parameter types, same return type. Extra helper methods on the struct are fine; the contract is just that the declared method is there with the right shape.

Try this. Define capability StoreUser with a single method save(self, id: String, payload: String) -> Result<Unit, IoError>. Implement it with a struct FileStore that holds an Fs and writes one file per user. In main, narrow the Fs to "/var/users/" before constructing the FileStore.

Where you are now

You can define your own capability types and implement them in terms of the built-ins. Authority surfaces in your programs can speak the language of your domain (StoreUser, SendEmail) instead of the language of the runtime (Fs, Net).

The next chapter moves out of single-file programs: how to split your code into modules, control which names are visible, and find dependencies. Then chapter 12 puts it all together in a real CLI tool.