Stack Module with Signature
Functional Programming
Tutorial
The Problem
Implement a persistent functional stack: a data structure where push and pop return new stacks rather than mutating in place. Model the OCaml module signature pattern in Rust's type system.
🎯 Learning Outcomes
self by value) so each operation returns a new stackResult to handle the empty-stack error case that OCaml raises as an exceptionCode Example
#![allow(clippy::all)]
//! Stack Module with Signature
//! See example.ml for OCaml reference
//!
//! Mirrors OCaml's `ListStack` module: push/pop consume and return a new stack,
//! giving a persistent, functional interface over a `Vec` backbone.
#[derive(Debug, Clone, PartialEq)]
pub struct Stack<T> {
items: Vec<T>,
}
#[derive(Debug, PartialEq)]
pub struct Empty;
impl<T> Stack<T> {
/// The empty stack.
pub fn empty() -> Self {
Stack { items: Vec::new() }
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn size(&self) -> usize {
self.items.len()
}
/// Push an element, returning the new stack (consuming `self`).
pub fn push(mut self, x: T) -> Self {
self.items.push(x);
self
}
/// Return a reference to the top element without removing it.
pub fn peek(&self) -> Result<&T, Empty> {
self.items.last().ok_or(Empty)
}
/// Remove the top element, returning the remainder (consuming `self`).
pub fn pop(mut self) -> Result<Self, Empty> {
if self.items.is_empty() {
Err(Empty)
} else {
self.items.pop();
Ok(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let s: Stack<i32> = Stack::empty();
assert!(s.is_empty());
assert_eq!(s.size(), 0);
}
#[test]
fn test_push_peek_size() {
let s = Stack::empty().push(1).push(2).push(3);
assert_eq!(s.size(), 3);
assert_eq!(s.peek(), Ok(&3));
assert!(!s.is_empty());
}
#[test]
fn test_pop() {
let s = Stack::empty().push(1).push(2).push(3);
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&2));
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&1));
let s = s.pop().unwrap();
assert!(s.is_empty());
}
#[test]
fn test_peek_empty_err() {
let s: Stack<i32> = Stack::empty();
assert_eq!(s.peek(), Err(Empty));
}
#[test]
fn test_pop_empty_err() {
let s: Stack<i32> = Stack::empty();
assert!(s.pop().is_err());
}
#[test]
fn test_pipeline_style() {
// Mirror OCaml: empty |> push 1 |> push 2 |> push 3
let s = Stack::empty().push(1).push(2).push(3);
assert_eq!(s.peek(), Ok(&3));
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&2));
}
}Key Differences
Empty exception on peek/pop; Rust returns Result<_, Empty>, making the error case explicit in the typeVec-backed implementation clones on each push/pop call for logical persistenceOCaml Approach
OCaml defines a STACK module signature and implements it with ListStack, backed by a list. push is x :: s (prepend), pop returns the tail, peek returns the head. Errors raise the Empty exception rather than returning an option type.
Full Source
#![allow(clippy::all)]
//! Stack Module with Signature
//! See example.ml for OCaml reference
//!
//! Mirrors OCaml's `ListStack` module: push/pop consume and return a new stack,
//! giving a persistent, functional interface over a `Vec` backbone.
#[derive(Debug, Clone, PartialEq)]
pub struct Stack<T> {
items: Vec<T>,
}
#[derive(Debug, PartialEq)]
pub struct Empty;
impl<T> Stack<T> {
/// The empty stack.
pub fn empty() -> Self {
Stack { items: Vec::new() }
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn size(&self) -> usize {
self.items.len()
}
/// Push an element, returning the new stack (consuming `self`).
pub fn push(mut self, x: T) -> Self {
self.items.push(x);
self
}
/// Return a reference to the top element without removing it.
pub fn peek(&self) -> Result<&T, Empty> {
self.items.last().ok_or(Empty)
}
/// Remove the top element, returning the remainder (consuming `self`).
pub fn pop(mut self) -> Result<Self, Empty> {
if self.items.is_empty() {
Err(Empty)
} else {
self.items.pop();
Ok(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let s: Stack<i32> = Stack::empty();
assert!(s.is_empty());
assert_eq!(s.size(), 0);
}
#[test]
fn test_push_peek_size() {
let s = Stack::empty().push(1).push(2).push(3);
assert_eq!(s.size(), 3);
assert_eq!(s.peek(), Ok(&3));
assert!(!s.is_empty());
}
#[test]
fn test_pop() {
let s = Stack::empty().push(1).push(2).push(3);
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&2));
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&1));
let s = s.pop().unwrap();
assert!(s.is_empty());
}
#[test]
fn test_peek_empty_err() {
let s: Stack<i32> = Stack::empty();
assert_eq!(s.peek(), Err(Empty));
}
#[test]
fn test_pop_empty_err() {
let s: Stack<i32> = Stack::empty();
assert!(s.pop().is_err());
}
#[test]
fn test_pipeline_style() {
// Mirror OCaml: empty |> push 1 |> push 2 |> push 3
let s = Stack::empty().push(1).push(2).push(3);
assert_eq!(s.peek(), Ok(&3));
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&2));
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let s: Stack<i32> = Stack::empty();
assert!(s.is_empty());
assert_eq!(s.size(), 0);
}
#[test]
fn test_push_peek_size() {
let s = Stack::empty().push(1).push(2).push(3);
assert_eq!(s.size(), 3);
assert_eq!(s.peek(), Ok(&3));
assert!(!s.is_empty());
}
#[test]
fn test_pop() {
let s = Stack::empty().push(1).push(2).push(3);
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&2));
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&1));
let s = s.pop().unwrap();
assert!(s.is_empty());
}
#[test]
fn test_peek_empty_err() {
let s: Stack<i32> = Stack::empty();
assert_eq!(s.peek(), Err(Empty));
}
#[test]
fn test_pop_empty_err() {
let s: Stack<i32> = Stack::empty();
assert!(s.pop().is_err());
}
#[test]
fn test_pipeline_style() {
// Mirror OCaml: empty |> push 1 |> push 2 |> push 3
let s = Stack::empty().push(1).push(2).push(3);
assert_eq!(s.peek(), Ok(&3));
let s = s.pop().unwrap();
assert_eq!(s.peek(), Ok(&2));
}
}
Exercises
Stack backed by a singly-linked list using Option<Box<Node<T>>> instead of Vec<T> for O(1) push and pop without cloningpeek_n(n) method that returns a reference to the nth element from the top without removing anythingFrom<Vec<T>> for Stack<T> and From<Stack<T>> for Vec<T> to make the stack interoperable with standard collections