Linear and Affine Types
March’s type system tracks ownership through linear and affine qualifiers. These let the compiler catch resource leaks and use-after-free bugs at compile time — not at runtime, and not by relying on a garbage collector.
The Problem They Solve
Consider a file handle or database connection. These resources must be:
- Used — you shouldn’t open a file and forget to read or close it
- Closed exactly once — closing twice is a bug
- Not shared — concurrent access through the same raw handle leads to data corruption
In most languages, these are programmer responsibilities enforced by convention and code review. In March, the type system enforces them.
Linear vs Affine
| Qualifier | Usage count | Meaning |
|---|---|---|
linear |
Exactly once | Must be used — dropping it is a compile error |
affine |
At most once | May be dropped (unused), but cannot be used twice |
Both prevent duplicating (using twice). Linear additionally prevents discarding (never using).
Linear Values
A linear type must be used exactly once:
fn consume(linear h : Handle) : () do
-- h must be used here — the compiler tracks this
close(h)
end
If you forget to use a linear value, the compiler reports an error:
fn bad(linear h : Handle) : () do
() -- error: linear value `h` must be used
end
If you try to use it twice:
fn also_bad(linear h : Handle) : () do
close(h)
close(h) -- error: linear value `h` used more than once
end
Linear Let Bindings
fn read_file(path : String) : String do
linear let handle : Handle = open_file(path)
let content = read_all(handle) -- consumes handle
content
end
The linear let annotation tells the compiler this binding has linear semantics. You don’t need to annotate everything — the type of open_file already carries the linear constraint.
Affine Values
An affine type may be used zero or one times. This is useful for values that have a cleanup operation but where “not using” is acceptable (e.g., an optional connection):
fn maybe_connect(affine cap : NetworkCap) : () do
-- OK to drop cap without using it
if should_connect do
connect(cap)
end
-- No error if we fall through without using cap
end
The key property: you still cannot use an affine value twice.
Linear Record Fields
Individual fields of a record can be linear:
type Resource = {
linear fd : FileDesc,
metadata : String
}
The compiler tracks each linear field independently. Accessing r.fd consumes that field — you cannot access it again.
Linear Types and Actors
Actors communicate by message passing. For safety, a linear value cannot be sent as a message directly — sending would require copying, and copying a linear value violates the uniqueness guarantee.
The actor system uses session types (see below) for typed communication channels where linear values can be transferred safely via Send/Recv channel handles.
Session Types
Session types use binary typed channels — the two endpoints have dual types. If one end sends, the other must receive.
Define a protocol:
protocol Transfer do
Client -> Server : Request(String)
Server -> Client : Response(Int)
end
Using a session channel:
fn client_side(send_ch : Send(String), recv_ch : Recv(Int)) do
linear let s = send_ch
linear let r = recv_ch
let s2 = Chan.send(s, "query") -- s consumed, returns continuation
let (result, _) = Chan.recv(r) -- r consumed, returns (value, continuation)
println("result: " ++ int_to_string(result))
end
The channel handles are linear — each send/recv operation consumes the old handle and returns a new one representing the next step of the protocol. The compiler verifies the full protocol is followed.
Capabilities as Linear Types
Capabilities (see capabilities.march) use linear types to ensure a capability token cannot be forged or duplicated:
fn read_secret(linear cap : Cap(Vault)) : String do
Vault.read(cap, "secret_key")
-- cap is consumed; caller must obtain a new one for further operations
end
Capability narrowing attenuates a capability to a sub-capability:
fn restricted_op(cap : Cap(IO)) : () do
let console_cap = cap_narrow(cap) -- Cap(IO) -> Cap(IO.Console)
greet(console_cap, "Alice")
end
FFI and Linear Pointers
When calling C code, raw pointers are typed as linear Ptr(a):
extern "libc": Cap(LibC) do
fn malloc(n : Int) : linear Ptr(a)
fn free(linear ptr : Ptr(a)) : ()
end
This makes memory management explicit in the type — you cannot forget to free a linear Ptr, and you cannot free it twice.
Practical Rules
-
Use
linearfor resources with mandatory cleanup — file handles, database connections, exclusive locks, capabilities you must return. -
Use
affinefor optional-use tokens — things you might or might not use, but definitely shouldn’t use twice. -
Ordinary values need no qualifier — the default is unrestricted (can be copied, dropped, used many times).
-
Pattern matching on a linear value consumes it — each branch must use it in a compatible way.
-
Linear fields in records — accessing the field consumes it; you must use or explicitly drop each linear field.
Why Both?
Many systems have only one kind of linear type. March has both because they solve different problems:
linearensures you can’t forget to do something (close, release, respond)affineensures you can’t duplicate something, while allowing graceful abandonment
For example, a session channel must be completed (linear — you can’t just drop it midway through a protocol). But an optional permission token might be affine — the operation is valid with or without it.
Next Steps
- Type System — the broader type system context
- Actors — how linear types interact with actor message passing
- Pattern Matching — destructuring linear values