Linear Types

A linear value must be used exactly once — the compiler rejects code that drops it without consuming it, or uses it twice. This makes resource leaks and double-frees impossible to write.


Declaring a linear type

linear type FileHandle = FileHandle(Int)

Every binding of FileHandle is automatically linear. Dropping it without calling a consuming function is a compile error:

fn main() do
  let fh = open_file("data.txt")
  -- returning here without using fh would be a compile error:
  -- "linear value `fh` is dropped without being consumed"
  let content = read_all(fh)   -- consumes fh
  println(content)
end

Always-linear types

always_linear type promotes every binding to linear without per-use-site annotations:

always_linear type DbConn = DbConn(Int)

Useful when you want the linearity guarantee enforced everywhere in the codebase, not just where the author remembered to write linear let.


Typestate: handles that track state

Handle(Resource, State) is a built-in always-linear typestate handle. State transitions are declared with transitions:

tag Open
tag Closed

transitions FileHandle do
  Closed: Closed -> Open   via open_file
  Open:   Open   -> Closed via close_file
  Open:   Open   -> Open   via read_chunk
end

Calling read_chunk on a FileHandle(Closed) is a compile error. The type system tracks exactly which state the handle is in at every point:

fn process(path : String) : Result(String, String) do
  let? fh      = open_file(path)          -- FileHandle(Open)
  let? content = read_chunk(fh)           -- FileHandle(Open), returns (data, FileHandle(Open))
  close_file(content.handle)              -- FileHandle(Open) -> Closed
  Ok(content.data)
end

with for linear resource scopes

with pairs acquisition with guaranteed cleanup — useful when you want RAII-style deterministic release:

fn read_file(path : String) : Result(String, String) do
  with Ok(fh) <- open_file(path) do
    let content = read_all(fh)
    close_file(fh)
    Ok(content)
  else
    Err(e) -> Err(e)
  end
end

Complete example: safe socket lifecycle

mod Net do
  always_linear type Socket = Socket(Int)

  tag Connected
  tag Disconnected

  transitions Socket do
    Disconnected: Disconnected -> Connected    via connect
    Connected:    Connected    -> Disconnected via disconnect
    Connected:    Connected    -> Connected    via send_bytes
    Connected:    Connected    -> Connected    via recv_bytes
  end

  fn echo_once(addr : String) : Result((), String) do
    let? sock         = connect(addr)
    let? (msg, sock2) = recv_bytes(sock)
    let? sock3        = send_bytes(sock2, msg)
    disconnect(sock3)
    Ok(())
  end
end

recv_bytes consumes sock and returns (data, Socket(Connected)) — a fresh handle. Using the original sock after that would be a compile error (linear value used twice). Forgetting to call disconnect at the end would also be a compile error — sock3 would be dropped without being consumed.

march — interactive
Click run on any snippet to try it here.
march>