Owning References Pattern
Tutorial Video
Text description (accessibility)
This video demonstrates the "Owning References Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Sometimes you want a type that owns its data and simultaneously exposes a view into it — a buffer that knows which window of its bytes is "active," a string that knows where its meaningful content starts and ends. Key difference from OCaml: 1. **Zero
Tutorial
The Problem
Sometimes you want a type that owns its data and simultaneously exposes a view into it — a buffer that knows which window of its bytes is "active," a string that knows where its meaningful content starts and ends. Storing both the owner and a reference to its contents in the same struct leads to self-referential problems. The owning-reference pattern solves this by storing indices rather than pointers, computing views on demand, or using separate types for owner and view.
🎯 Learning Outcomes
OwnedSlice stores indices instead of references to avoid self-referential issuesfn slice(&self) -> &[u8] computes the view from stored indices at access timefn narrow(&mut self, start, end) adjusts the active window without copying dataCode Example
#![allow(clippy::all)]
//! Owning References Pattern
//!
//! Combining ownership with interior borrowing.
/// Owner with cached view.
pub struct OwnedSlice {
data: Vec<u8>,
start: usize,
end: usize,
}
impl OwnedSlice {
pub fn new(data: Vec<u8>) -> Self {
let end = data.len();
OwnedSlice {
data,
start: 0,
end,
}
}
pub fn slice(&self) -> &[u8] {
&self.data[self.start..self.end]
}
pub fn narrow(&mut self, start: usize, end: usize) {
self.start = start.min(self.data.len());
self.end = end.min(self.data.len());
}
}
/// String with owned and view.
pub struct OwnedStr {
data: String,
}
impl OwnedStr {
pub fn new(s: &str) -> Self {
OwnedStr {
data: s.to_string(),
}
}
pub fn as_str(&self) -> &str {
&self.data
}
pub fn into_string(self) -> String {
self.data
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_owned_slice() {
let mut slice = OwnedSlice::new(vec![1, 2, 3, 4, 5]);
assert_eq!(slice.slice(), &[1, 2, 3, 4, 5]);
slice.narrow(1, 4);
assert_eq!(slice.slice(), &[2, 3, 4]);
}
#[test]
fn test_owned_str() {
let s = OwnedStr::new("hello");
assert_eq!(s.as_str(), "hello");
assert_eq!(s.into_string(), "hello");
}
}Key Differences
&self.data[s..e] is a true zero-copy view into the Vec; OCaml Bytes.sub copies — zero-copy needs Bigarray.slice() result cannot outlive self; OCaml's GC keeps Bigarray slices alive but does not prevent use-after-mutation.narrow(&mut self) requires &mut self — the compiler prevents concurrent reads of the slice while narrowing; OCaml allows concurrent access through ref.ropey) use owning-reference patterns extensively; OCaml text editors use GC-managed trees of string chunks.OCaml Approach
OCaml's Bytes or Bigarray slices are reference-counted or GC-managed, so owning references are natural:
type owned_slice = { data: bytes; mutable start: int; mutable end_: int }
let slice s = Bytes.sub s.data s.start (s.end_ - s.start) (* copies *)
(* Zero-copy requires Bigarray.Array1 sub-views *)
True zero-copy sub-views in OCaml require Bigarray.Array1 with explicit layout management.
Full Source
#![allow(clippy::all)]
//! Owning References Pattern
//!
//! Combining ownership with interior borrowing.
/// Owner with cached view.
pub struct OwnedSlice {
data: Vec<u8>,
start: usize,
end: usize,
}
impl OwnedSlice {
pub fn new(data: Vec<u8>) -> Self {
let end = data.len();
OwnedSlice {
data,
start: 0,
end,
}
}
pub fn slice(&self) -> &[u8] {
&self.data[self.start..self.end]
}
pub fn narrow(&mut self, start: usize, end: usize) {
self.start = start.min(self.data.len());
self.end = end.min(self.data.len());
}
}
/// String with owned and view.
pub struct OwnedStr {
data: String,
}
impl OwnedStr {
pub fn new(s: &str) -> Self {
OwnedStr {
data: s.to_string(),
}
}
pub fn as_str(&self) -> &str {
&self.data
}
pub fn into_string(self) -> String {
self.data
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_owned_slice() {
let mut slice = OwnedSlice::new(vec![1, 2, 3, 4, 5]);
assert_eq!(slice.slice(), &[1, 2, 3, 4, 5]);
slice.narrow(1, 4);
assert_eq!(slice.slice(), &[2, 3, 4]);
}
#[test]
fn test_owned_str() {
let s = OwnedStr::new("hello");
assert_eq!(s.as_str(), "hello");
assert_eq!(s.into_string(), "hello");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_owned_slice() {
let mut slice = OwnedSlice::new(vec![1, 2, 3, 4, 5]);
assert_eq!(slice.slice(), &[1, 2, 3, 4, 5]);
slice.narrow(1, 4);
assert_eq!(slice.slice(), &[2, 3, 4]);
}
#[test]
fn test_owned_str() {
let s = OwnedStr::new("hello");
assert_eq!(s.as_str(), "hello");
assert_eq!(s.into_string(), "hello");
}
}
Deep Comparison
OCaml vs Rust: lifetime owning ref
See example.rs and example.ml for implementations.
Key Differences
Exercises
struct Packet { data: Vec<u8>, pos: usize } with a fn read_u16(&mut self) -> Option<u16> that reads two bytes at pos and advances it.OwnedSlice to wrap around: fn wrap_slice(&self) -> (&[u8], &[u8]) returning the two parts when start > end (ring buffer state).fn split_at(&self, mid: usize) -> (OwnedSliceView<'_>, OwnedSliceView<'_>) returning two views into the same OwnedSlice without copying.