🦀 Functional Rust

146: Opaque Types — Hiding Implementation Details

Difficulty: 3 Level: Intermediate Expose an interface without revealing how it's built — callers get the what, not the how.

The Problem This Solves

Good API design separates interface from implementation. If your `Stack<T>` is internally a `Vec<T>`, clients should not be able to call `.sort()` on it or depend on vector-specific behavior. You want them to use only the operations you've designed: `push`, `pop`, `peek`. Exposing internals creates maintenance traps: every time you change the implementation, you risk breaking clients. Hiding them gives you freedom to refactor, swap data structures, or add caching — without touching callers. Rust has two distinct mechanisms: private fields (module-level) for hiding the type representation, and `impl Trait` return types for hiding the concrete type of a return value.

The Intuition

Private fields: A struct with a private inner `Vec<T>` forces all access through the public methods you define. Outside the module, there's no way to access the underlying Vec — not even for reading. The type is opaque. `impl Trait` returns: When a function returns `impl Iterator<Item = i32>`, the caller knows it's some iterator that yields `i32` values. They don't know if it's a `Map`, a `Filter`, a `Chain`, or a custom type. This freedom is valuable: you can change the return type without changing the function signature.

How It Works in Rust

pub mod stack {
 /// Callers see Stack<T> as an opaque type.
 /// The inner Vec is private — no direct access outside this module.
 #[derive(Debug, Clone)]
 pub struct Stack<T>(Vec<T>);  // tuple struct: field is private by default

 impl<T> Stack<T> {
     pub fn empty() -> Self { Stack(Vec::new()) }

     pub fn push(mut self, x: T) -> Self {
         self.0.push(x);
         self
     }

     pub fn pop(mut self) -> (Option<T>, Self) {
         let top = self.0.pop();
         (top, self)
     }

     pub fn peek(&self) -> Option<&T> { self.0.last() }
     pub fn size(&self) -> usize { self.0.len() }
 }
 // Outside this module: s.0 would be a compile error
}
`impl Trait` opaque return type:
// Caller sees "some iterator of i32" — not the concrete type
fn make_counter(start: i32, step: i32) -> impl Iterator<Item = i32> {
 (0..).map(move |i| start + i * step)
 // Could be changed to a different iterator type without breaking callers
}

let counter: Vec<i32> = make_counter(10, 5).take(4).collect();
// [10, 15, 20, 25]
Validated opaque type — can only be constructed via a checked constructor:
pub struct SecretKey(Vec<u8>);  // field is private

impl SecretKey {
 pub fn new(key: &[u8]) -> Result<Self, &'static str> {
     if key.len() >= 16 {
         Ok(SecretKey(key.to_vec()))  // only valid keys can be created
     } else {
         Err("key must be at least 16 bytes")
     }
 }
 // No way to construct SecretKey without going through `new`
}

What This Unlocks

Key Differences

ConceptOCamlRust
Hide type representationModule signature with abstract type: `type t`Private fields in struct (module-level privacy)
Opaque return typeNot directly expressible`impl Trait` in function return position
Constructor validationModule function, abstract type prevents bypassPrivate field + public constructor
Sealing a traitFirst-class modules / private signaturePrivate supertrait (sealed trait pattern)
Scope of privacySignature/module boundary`pub(crate)`, `pub(super)`, or fully private
// Opaque types: the type's representation is hidden from callers.
// Only the declared interface is accessible; the concrete implementation
// cannot be inspected or constructed directly.
//
// Rust achieves this via:
//   - Structs with private fields (module-level privacy)
//   - `impl Trait` return types (opaque impl in function signatures)
//   - Sealed traits (callers can use but not implement — see 148)

// ── Stack with private representation ────────────────────────────────────────

pub mod stack {
    /// An opaque LIFO stack. Callers see only `Stack<T>`, not its internals.
    #[derive(Debug, Clone)]
    pub struct Stack<T>(Vec<T>); // inner Vec is private

    impl<T> Stack<T> {
        pub fn empty() -> Self {
            Stack(Vec::new())
        }

        pub fn push(mut self, x: T) -> Self {
            self.0.push(x);
            self
        }

        pub fn pop(mut self) -> (Option<T>, Self) {
            let top = self.0.pop();
            (top, self)
        }

        pub fn peek(&self) -> Option<&T> {
            self.0.last()
        }

        pub fn size(&self) -> usize {
            self.0.len()
        }

        pub fn is_empty(&self) -> bool {
            self.0.is_empty()
        }
    }
}

// ── Queue with private representation ────────────────────────────────────────

pub mod queue {
    /// A FIFO queue backed by two stacks (amortised O(1) enqueue/dequeue).
    #[derive(Debug, Clone)]
    pub struct Queue<T> {
        inbox:  Vec<T>,
        outbox: Vec<T>,
    }

    impl<T> Queue<T> {
        pub fn empty() -> Self {
            Queue { inbox: Vec::new(), outbox: Vec::new() }
        }

        pub fn enqueue(mut self, x: T) -> Self {
            self.inbox.push(x);
            self
        }

        pub fn dequeue(mut self) -> (Option<T>, Self) {
            if self.outbox.is_empty() {
                while let Some(x) = self.inbox.pop() {
                    self.outbox.push(x);
                }
            }
            let item = self.outbox.pop();
            (item, self)
        }

        pub fn peek(&mut self) -> Option<&T> {
            if self.outbox.is_empty() {
                while let Some(x) = self.inbox.pop() {
                    self.outbox.push(x);
                }
            }
            self.outbox.last()
        }

        pub fn size(&self) -> usize {
            self.inbox.len() + self.outbox.len()
        }

        pub fn is_empty(&self) -> bool {
            self.inbox.is_empty() && self.outbox.is_empty()
        }
    }
}

// ── `impl Trait` opaque return types ─────────────────────────────────────────
// The caller sees `impl Iterator<Item = i32>` but not the concrete type.

fn make_counter(start: i32, step: i32) -> impl Iterator<Item = i32> {
    // Could be any iterator internally; caller cannot name the type.
    (0..).map(move |i| start + i * step)
}

fn make_even_squares() -> impl Iterator<Item = u64> {
    (1u64..).map(|n| n * n).filter(|n| n % 2 == 0)
}

/// A secret key type — callers can't construct one directly.
pub struct SecretKey(Vec<u8>);

impl SecretKey {
    /// Only way to get a SecretKey: go through this validated constructor.
    pub fn new(key: &[u8]) -> Result<Self, &'static str> {
        if key.len() >= 16 {
            Ok(SecretKey(key.to_vec()))
        } else {
            Err("key must be at least 16 bytes")
        }
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn xor_encrypt(&self, data: &[u8]) -> Vec<u8> {
        data.iter()
            .zip(self.0.iter().cycle())
            .map(|(d, k)| d ^ k)
            .collect()
    }
}

fn main() {
    use stack::Stack;
    use queue::Queue;

    // ── Stack ─────────────────────────────────────────────────────────────────
    let s = Stack::empty()
        .push(1)
        .push(2)
        .push(3);

    println!("size: {}", s.size());
    println!("peek: {:?}", s.peek());

    let (top, rest) = s.pop();
    println!("popped: {:?}, new size: {}", top, rest.size());
    println!("empty: {}", Stack::<i32>::empty().is_empty());

    // ── Queue ─────────────────────────────────────────────────────────────────
    let q = Queue::empty()
        .enqueue("first")
        .enqueue("second")
        .enqueue("third");

    println!("\nQueue size: {}", q.size());
    let (item, q2) = q.dequeue();
    println!("dequeued: {:?}", item);
    println!("remaining: {}", q2.size());

    // ── impl Trait opaque returns ─────────────────────────────────────────────
    let counter: Vec<i32> = make_counter(10, 5).take(4).collect();
    println!("\ncounter(10, step=5): {:?}", counter);

    let squares: Vec<u64> = make_even_squares().take(5).collect();
    println!("first 5 even squares: {:?}", squares);

    // ── Validated opaque SecretKey ────────────────────────────────────────────
    match SecretKey::new(b"this-is-a-valid-key-32-bytes-long!") {
        Ok(key) => {
            println!("\nkey len: {}", key.len());
            let ct = key.xor_encrypt(b"hello");
            let pt = key.xor_encrypt(&ct);
            println!("xor round-trip: {:?}", std::str::from_utf8(&pt).unwrap());
        }
        Err(e) => println!("Key error: {}", e),
    }
    println!("short key: {:?}", SecretKey::new(b"short").err());
}

#[cfg(test)]
mod tests {
    use super::stack::Stack;
    use super::queue::Queue;
    use super::{SecretKey, make_counter};

    #[test]
    fn test_stack_push_pop() {
        let s = Stack::empty().push(10).push(20).push(30);
        assert_eq!(s.size(), 3);
        let (top, rest) = s.pop();
        assert_eq!(top, Some(30));
        assert_eq!(rest.size(), 2);
    }

    #[test]
    fn test_stack_empty() {
        let s = Stack::<i32>::empty();
        assert!(s.is_empty());
        let (top, _) = s.pop();
        assert_eq!(top, None);
    }

    #[test]
    fn test_queue_fifo() {
        let q = Queue::empty().enqueue(1).enqueue(2).enqueue(3);
        let (a, q) = q.dequeue();
        let (b, q) = q.dequeue();
        let (c, _) = q.dequeue();
        assert_eq!((a, b, c), (Some(1), Some(2), Some(3)));
    }

    #[test]
    fn test_impl_trait_counter() {
        let v: Vec<i32> = make_counter(0, 2).take(5).collect();
        assert_eq!(v, vec![0, 2, 4, 6, 8]);
    }

    #[test]
    fn test_secret_key_validation() {
        assert!(SecretKey::new(b"this-is-16-bytes").is_ok());
        assert!(SecretKey::new(b"short").is_err());
    }

    #[test]
    fn test_xor_encrypt_decrypt() {
        let key = SecretKey::new(b"sixteen-byte-key").unwrap();
        let data = b"hello world";
        let encrypted = key.xor_encrypt(data);
        let decrypted = key.xor_encrypt(&encrypted);
        assert_eq!(decrypted, data);
    }
}
(* Opaque types: the type's representation is hidden by the module signature.
   Callers can only use the type through the provided interface. *)

module type STACK = sig
  type 'a t
  val empty  : 'a t
  val push   : 'a -> 'a t -> 'a t
  val pop    : 'a t -> ('a * 'a t) option
  val peek   : 'a t -> 'a option
  val size   : 'a t -> int
  val is_empty : 'a t -> bool
end

(* List-based implementation — type is opaque outside this module *)
module Stack : STACK = struct
  type 'a t = 'a list
  let empty       = []
  let push x s    = x :: s
  let pop = function
    | []    -> None
    | x :: xs -> Some (x, xs)
  let peek = function
    | []    -> None
    | x :: _ -> Some x
  let size        = List.length
  let is_empty    = (= [])
end

let () =
  let s = Stack.(push 3 (push 2 (push 1 empty))) in
  Printf.printf "size: %d\n"    (Stack.size s);
  Printf.printf "peek: %d\n"    (Option.get (Stack.peek s));
  (match Stack.pop s with
   | Some (top, rest) ->
     Printf.printf "popped: %d, new size: %d\n" top (Stack.size rest)
   | None -> assert false);
  Printf.printf "empty: %b\n"   (Stack.is_empty Stack.empty)

📊 Detailed Comparison

Comparison: Opaque Types

OCaml

🐪 Show OCaml equivalent
module type STACK = sig
type 'a t           (* opaque! *)
val empty : 'a t
val push : 'a -> 'a t -> 'a t
val pop : 'a t -> ('a * 'a t) option
end

module ListStack : STACK = struct
type 'a t = 'a list  (* hidden outside module *)
let empty = []
let push x s = x :: s
let pop = function [] -> None | x :: r -> Some (x, r)
end

Rust

// Module privacy for opaque types
mod stack {
 pub struct Stack<T>(Vec<T>);  // Vec<T> is private
 impl<T> Stack<T> {
     pub fn new() -> Self { Stack(Vec::new()) }
     pub fn push(&mut self, val: T) { self.0.push(val); }
 }
}

// impl Trait for opaque returns
fn fibonacci() -> impl Iterator<Item = u64> {
 // Concrete type hidden from caller
}