๐Ÿฆ€ Functional Rust

736: Typestate Connection

Difficulty: 4 Level: Expert Model a TCP connection lifecycle as a typestate machine โ€” `send()` and `recv()` exist only on `TcpConn<Connected>`, `connect()` only on `TcpConn<Disconnected>` โ€” making invalid usage a compile error.

The Problem This Solves

Network connections have a strict lifecycle: connect before send, don't send after close, don't connect twice. Runtime state machines enforce this with `match self.state { ... }` guards and `Result` errors for invalid transitions. These guards must be written for every method, can be forgotten, and only fail at runtime โ€” when your test suite is incomplete, they fail in production. API consumers face the same problem from the outside: nothing in the type signature of `fn send(&mut self, data: &[u8])` tells you that the connection must be in a connected state. The precondition is invisible until you read the docs, get a runtime error, or study the implementation. The typestate pattern makes these constraints visible and enforced in the type signature. `send()` is defined on `TcpConn<Connected>`, not on `TcpConn<Disconnected>` or `TcpConn<Closed>`. Call it at the wrong time and the compiler tells you immediately. The documentation is the type.

The Intuition

The state lives in the type, not in a field. `TcpConn<Disconnected>` and `TcpConn<Connected>` are different Rust types โ€” they have different methods and cannot be used interchangeably. The state parameter is a `PhantomData<State>` โ€” zero bytes, purely a type-level annotation. Transitions consume the connection and return a new one with a different state type. `conn.connect()` takes `TcpConn<Disconnected>` by value and returns `Result<TcpConn<Connected>, String>`. After `.connect()`, the original `conn` variable is moved โ€” you can't accidentally use the disconnected version. Rust's ownership system enforces single-state use automatically.

How It Works in Rust

use std::marker::PhantomData;

// State markers โ€” zero-sized, carry no data
pub struct Disconnected;
pub struct Connected;
pub struct Closed;

pub struct TcpConn<State> {
 host: String,
 port: u16,
 bytes_sent: usize,
 bytes_recv: usize,
 _state: PhantomData<State>,  // zero bytes; tracks state in the type
}

impl TcpConn<Disconnected> {
 pub fn new(host: impl Into<String>, port: u16) -> Self { /* ... */ }

 // Consuming transition: Disconnected โ†’ Connected
 pub fn connect(self) -> Result<TcpConn<Connected>, String> {
     println!("Connecting to {}:{}", self.host, self.port);
     Ok(TcpConn { _state: PhantomData, ..self })
 }
 // No send(), recv(), or close() here โ€” Disconnected can't do those
}

impl TcpConn<Connected> {
 // send() only on Connected โ€” consumes self, returns self (same state)
 pub fn send(mut self, data: &[u8]) -> Result<TcpConn<Connected>, String> {
     self.bytes_sent += data.len();
     Ok(self)
 }

 pub fn recv(mut self) -> Result<(Vec<u8>, TcpConn<Connected>), String> {
     let data = b"HTTP/1.1 200 OK\r\n".to_vec();
     self.bytes_recv += data.len();
     Ok((data, self))
 }

 // Consuming transition: Connected โ†’ Closed
 pub fn close(self) -> TcpConn<Closed> {
     TcpConn { _state: PhantomData, ..self }
 }
}

impl TcpConn<Closed> {
 pub fn bytes_sent(&self) -> usize { self.bytes_sent }
 pub fn bytes_recv(&self) -> usize { self.bytes_recv }
 // No send(), recv(), or connect() โ€” Closed is terminal
}

// โ”€โ”€ Valid usage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
let conn = TcpConn::<Disconnected>::new("example.com", 80);
let conn = conn.connect()?;
let conn = conn.send(b"GET / HTTP/1.1\r\n")?;
let (response, conn) = conn.recv()?;
let closed = conn.close();

// โ”€โ”€ Compile errors for invalid usage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// closed.send(b"data");      // error: no method `send` on TcpConn<Closed>
// closed.connect();          // error: no method `connect` on TcpConn<Closed>
// let disc = TcpConn::<Disconnected>::new("x", 80);
// disc.send(b"data");        // error: no method `send` on TcpConn<Disconnected>

What This Unlocks

Key Differences

ConceptOCamlRust
State encodingVariant in a sum type (runtime); GADT for compile-timeZero-sized phantom type parameter โ€” zero runtime cost
Invalid operationRuntime exception or `Result` returnCompile error โ€” method doesn't exist on wrong type
State transitionFunction returning new state valueConsuming method returning new generic instantiation
Ownership of stateShared via ref-counting or explicit passingMoved โ€” original unavailable after transition
Connection pool`('a, connected) conn` lifetime approach`Vec<TcpConn<Connected>>` โ€” only connected conns
/// 736: TCP Connection modelled as typestate
/// Send/recv only available on Connected; connect only on Disconnected.

use std::marker::PhantomData;

// โ”€โ”€ State markers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
pub struct Disconnected;
pub struct Connecting;
pub struct Connected;
pub struct Closed;

// โ”€โ”€ Connection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub struct TcpConn<State> {
    host: String,
    port: u16,
    // In a real impl, this would hold a socket fd
    bytes_sent: usize,
    bytes_recv: usize,
    _state: PhantomData<State>,
}

impl TcpConn<Disconnected> {
    pub fn new(host: impl Into<String>, port: u16) -> Self {
        TcpConn {
            host: host.into(),
            port,
            bytes_sent: 0,
            bytes_recv: 0,
            _state: PhantomData,
        }
    }

    /// Transition: Disconnected โ†’ Connected
    pub fn connect(self) -> Result<TcpConn<Connected>, String> {
        println!("Connecting to {}:{} ...", self.host, self.port);
        // In reality: TcpStream::connect(...)
        Ok(TcpConn {
            host: self.host,
            port: self.port,
            bytes_sent: 0,
            bytes_recv: 0,
            _state: PhantomData,
        })
    }
}

impl TcpConn<Connected> {
    /// Send data โ€” only available when Connected.
    pub fn send(mut self, data: &[u8]) -> Result<TcpConn<Connected>, String> {
        println!("[{}:{}] โ†’ {} bytes", self.host, self.port, data.len());
        self.bytes_sent += data.len();
        Ok(self)
    }

    /// Receive data โ€” only available when Connected.
    pub fn recv(mut self) -> Result<(Vec<u8>, TcpConn<Connected>), String> {
        let fake_data = b"HTTP/1.1 200 OK\r\n".to_vec();
        println!("[{}:{}] โ† {} bytes", self.host, self.port, fake_data.len());
        self.bytes_recv += fake_data.len();
        Ok((fake_data, self))
    }

    /// Transition: Connected โ†’ Closed
    pub fn close(self) -> TcpConn<Closed> {
        println!("Closing {}:{} (sent={}, recv={})",
            self.host, self.port, self.bytes_sent, self.bytes_recv);
        TcpConn {
            host: self.host,
            port: self.port,
            bytes_sent: self.bytes_sent,
            bytes_recv: self.bytes_recv,
            _state: PhantomData,
        }
    }

    pub fn peer(&self) -> String {
        format!("{}:{}", self.host, self.port)
    }
}

impl TcpConn<Closed> {
    pub fn bytes_sent(&self) -> usize { self.bytes_sent }
    pub fn bytes_recv(&self) -> usize { self.bytes_recv }
}

fn main() {
    let conn = TcpConn::<Disconnected>::new("example.com", 80);

    let conn = conn.connect().expect("connect failed");
    println!("Connected to {}", conn.peer());

    let conn = conn.send(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
        .expect("send failed");
    let (response, conn) = conn.recv().expect("recv failed");
    println!("Response first line: {}",
        std::str::from_utf8(&response[..response.len().min(20)]).unwrap_or("?"));

    let closed = conn.close();
    println!("Totals: sent={}, recv={}", closed.bytes_sent(), closed.bytes_recv());

    // โ”€โ”€ These DO NOT COMPILE: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // closed.send(b"data");     // ERROR: method `send` not found on TcpConn<Closed>
    // closed.connect();         // ERROR: method `connect` not found on TcpConn<Closed>
    // let disc = TcpConn::<Disconnected>::new("x", 80);
    // disc.send(b"data");       // ERROR: method `send` not found on TcpConn<Disconnected>
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn connect_then_close() {
        let conn = TcpConn::<Disconnected>::new("localhost", 8080);
        let conn = conn.connect().unwrap();
        let closed = conn.close();
        assert_eq!(closed.bytes_sent(), 0);
        assert_eq!(closed.bytes_recv(), 0);
    }

    #[test]
    fn send_recv_accumulates_bytes() {
        let conn = TcpConn::<Disconnected>::new("localhost", 8080)
            .connect().unwrap();
        let conn = conn.send(b"hello world").unwrap();
        let (_data, conn) = conn.recv().unwrap();
        let closed = conn.close();
        assert_eq!(closed.bytes_sent(), 11);
        assert!(closed.bytes_recv() > 0);
    }

    #[test]
    fn peer_returns_host_and_port() {
        let conn = TcpConn::<Disconnected>::new("example.com", 443)
            .connect().unwrap();
        assert_eq!(conn.peer(), "example.com:443");
        conn.close();
    }
}
(* 736: TCP Connection as Typestate โ€” OCaml simulation *)

type disconnected = Disconnected
type connecting   = Connecting
type connected    = Connected
type closed       = Closed

(* We simulate state using a phantom wrapper *)
type 'state conn = {
  host: string;
  port: int;
  state_label: string;
}

let new_conn host port : disconnected conn =
  { host; port; state_label = "Disconnected" }

let connect (c: disconnected conn) : connected conn =
  Printf.printf "Connecting to %s:%d...\n" c.host c.port;
  { c with state_label = "Connected" }

let send (c: connected conn) msg : connected conn =
  Printf.printf "[%s:%d] Sent: %s\n" c.host c.port msg;
  c

let recv (c: connected conn) : (string * connected conn) =
  Printf.printf "[%s:%d] Recv: <data>\n" c.host c.port;
  ("response", c)

let close (c: connected conn) : closed conn =
  Printf.printf "Closing connection to %s:%d\n" c.host c.port;
  { c with state_label = "Closed" }

let () =
  let conn = new_conn "example.com" 80 in
  let conn = connect conn in
  let conn = send conn "GET / HTTP/1.1" in
  let (_data, conn) = recv conn in
  let _closed = close conn in
  ()
  (* Invalid: send conn "another message" -- OCaml would flag this for closed conn *)