Deref Coercions
Tutorial
The Problem
Rust has many smart pointer types — String, Vec<T>, Box<T>, Arc<T>, Rc<T> — each wrapping a more primitive type. Without automatic conversion, every function accepting &str would reject &String, forcing callers to write .as_str() everywhere. Deref coercions solve this by letting the compiler insert implicit dereferences: &String becomes &str, &Vec<T> becomes &[T], transitively. This gives ergonomic APIs without sacrificing type safety.
🎯 Learning Outcomes
Deref trait and how the compiler applies coercions automatically&str, &[T])Box<T>, Arc<T>, and Rc<T> participate in coercion chainsDeref on a custom wrapper type to integrate with the coercion systemCode Example
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
let s: String = String::from("Alice");
greet(&s); // &String → &str (one coercion)
let boxed: Box<String> = Box::new(String::from("Bob"));
greet(&boxed); // &Box<String> → &String → &str (two coercions)
let v: Vec<i32> = vec![1, 2, 3];
sum(&v); // &Vec<i32> → &[i32] (one coercion)Key Differences
Bytes.to_string).&T) — they never imply copying or ownership transfer.Deref on a Rust newtype makes it participate in the coercion chain; OCaml has no equivalent mechanism.&Box<Vec<u8>> → &Vec<u8> → &[u8]); in OCaml all such conversions must be written explicitly.OCaml Approach
OCaml does not have deref coercions — every type conversion is explicit. However, OCaml's module system and polymorphism reduce the need for them: a function taking string already accepts any string value directly without wrapping. OCaml does have subtyping for object types and variant inheritance via polymorphic variants, but these are unrelated to pointer coercions.
Full Source
#![allow(clippy::all)]
//! # Example 118: Deref Coercions
//!
//! The Rust compiler automatically dereferences smart pointers through the `Deref`
//! trait so you rarely need explicit conversions. `&String` becomes `&str`,
//! `&Vec<T>` becomes `&[T]`, and `&Box<T>` becomes `&T` — transitively.
//!
//! The idiomatic pattern is: **write functions that accept the most general borrow**
//! (`&str`, `&[T]`) and let callers pass any owning or smart-pointer form for free.
use std::ops::Deref;
use std::sync::Arc;
// ── Approach 1: Idiomatic — accept the most general borrowed form ─────────────
/// Accepts `&str` — callers may pass `&String`, `&Box<String>`, `&Rc<String>`, …
/// The compiler inserts as many `.deref()` calls as needed.
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
/// Accepts `&[i32]` — callers may pass `&Vec<i32>`, `&[i32; N]`, `&Box<Vec<i32>>`, …
pub fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
/// Generic first element — works with any slice-like type via deref coercion.
pub fn first<T>(items: &[T]) -> Option<&T> {
items.first()
}
// ── Approach 2: Custom Deref implementation ───────────────────────────────────
/// A newtype wrapper around `Vec<T>`.
///
/// By implementing `Deref<Target = [T]>`, it participates in coercion chains:
/// `&MyVec<T>` → `&[T]` automatically wherever `&[T]` is expected.
pub struct MyVec<T>(Vec<T>);
impl<T> MyVec<T> {
pub fn new(v: Vec<T>) -> Self {
Self(v)
}
}
impl<T> Deref for MyVec<T> {
type Target = [T];
fn deref(&self) -> &[T] {
&self.0
}
}
/// Works because `&MyVec<i32>` coerces to `&[i32]` via our `Deref` impl.
pub fn sum_my_vec(v: &MyVec<i32>) -> i32 {
sum(v)
}
// ── Approach 3: Rc / Arc chains ───────────────────────────────────────────────
/// Demonstrates `&Rc<String>` → `&String` → `&str` (two steps).
pub fn shout(name: &str) -> String {
format!("{}!", name.to_uppercase())
}
/// `&Arc<Vec<i32>>` → `&Vec<i32>` → `&[i32]` (two deref steps).
pub fn sum_arc(data: &Arc<Vec<i32>>) -> i32 {
sum(data)
}
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
// ── single-step coercions ─────────────────────────────────────────────────
#[test]
fn test_greet_str_literal() {
assert_eq!(greet("Alice"), "Hello, Alice!");
}
#[test]
fn test_greet_owned_string_coerces_to_str() {
let name = String::from("Bob");
assert_eq!(greet(&name), "Hello, Bob!"); // &String → &str
}
#[test]
fn test_sum_vec_coerces_to_slice() {
let v = vec![1, 2, 3, 4];
assert_eq!(sum(&v), 10); // &Vec<i32> → &[i32]
}
#[test]
fn test_sum_slice_literal() {
assert_eq!(sum(&[10, 20, 30]), 60);
}
#[test]
fn test_sum_empty() {
assert_eq!(sum(&[]), 0);
}
// ── transitive / two-step coercions ──────────────────────────────────────
#[test]
fn test_greet_box_string_two_step_coercion() {
// &Box<String> → &String → &str (two automatic deref steps)
let boxed: Box<String> = Box::new(String::from("Carol"));
assert_eq!(greet(&boxed), "Hello, Carol!");
}
#[test]
fn test_greet_rc_string_two_step_coercion() {
// &Rc<String> → &String → &str
let rc = Rc::new(String::from("Dave"));
assert_eq!(shout(&rc), "DAVE!"); // uses shout, same coercion
}
#[test]
fn test_sum_arc_vec_two_step_coercion() {
// &Arc<Vec<i32>> → &Vec<i32> → &[i32]
let arc = Arc::new(vec![5, 10, 15]);
assert_eq!(sum_arc(&arc), 30);
}
// ── custom Deref ──────────────────────────────────────────────────────────
#[test]
fn test_custom_deref_sum() {
let mv = MyVec::new(vec![5, 10, 15]);
assert_eq!(sum_my_vec(&mv), 30); // &MyVec<i32> → &[i32]
}
#[test]
fn test_custom_deref_first() {
let mv = MyVec::new(vec![7, 8, 9]);
assert_eq!(first(&mv), Some(&7)); // &MyVec<T> → &[T]
}
#[test]
fn test_custom_deref_empty() {
let mv: MyVec<i32> = MyVec::new(vec![]);
assert_eq!(first(&mv), None);
}
// ── method resolution through deref ──────────────────────────────────────
#[test]
fn test_method_called_on_box_resolves_through_deref() {
// Box<String> has no `.len()` method, but deref to String → str gives us one
let boxed = Box::new(String::from("hello"));
assert_eq!(boxed.len(), 5); // deref auto-applied for method calls too
}
}#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
// ── single-step coercions ─────────────────────────────────────────────────
#[test]
fn test_greet_str_literal() {
assert_eq!(greet("Alice"), "Hello, Alice!");
}
#[test]
fn test_greet_owned_string_coerces_to_str() {
let name = String::from("Bob");
assert_eq!(greet(&name), "Hello, Bob!"); // &String → &str
}
#[test]
fn test_sum_vec_coerces_to_slice() {
let v = vec![1, 2, 3, 4];
assert_eq!(sum(&v), 10); // &Vec<i32> → &[i32]
}
#[test]
fn test_sum_slice_literal() {
assert_eq!(sum(&[10, 20, 30]), 60);
}
#[test]
fn test_sum_empty() {
assert_eq!(sum(&[]), 0);
}
// ── transitive / two-step coercions ──────────────────────────────────────
#[test]
fn test_greet_box_string_two_step_coercion() {
// &Box<String> → &String → &str (two automatic deref steps)
let boxed: Box<String> = Box::new(String::from("Carol"));
assert_eq!(greet(&boxed), "Hello, Carol!");
}
#[test]
fn test_greet_rc_string_two_step_coercion() {
// &Rc<String> → &String → &str
let rc = Rc::new(String::from("Dave"));
assert_eq!(shout(&rc), "DAVE!"); // uses shout, same coercion
}
#[test]
fn test_sum_arc_vec_two_step_coercion() {
// &Arc<Vec<i32>> → &Vec<i32> → &[i32]
let arc = Arc::new(vec![5, 10, 15]);
assert_eq!(sum_arc(&arc), 30);
}
// ── custom Deref ──────────────────────────────────────────────────────────
#[test]
fn test_custom_deref_sum() {
let mv = MyVec::new(vec![5, 10, 15]);
assert_eq!(sum_my_vec(&mv), 30); // &MyVec<i32> → &[i32]
}
#[test]
fn test_custom_deref_first() {
let mv = MyVec::new(vec![7, 8, 9]);
assert_eq!(first(&mv), Some(&7)); // &MyVec<T> → &[T]
}
#[test]
fn test_custom_deref_empty() {
let mv: MyVec<i32> = MyVec::new(vec![]);
assert_eq!(first(&mv), None);
}
// ── method resolution through deref ──────────────────────────────────────
#[test]
fn test_method_called_on_box_resolves_through_deref() {
// Box<String> has no `.len()` method, but deref to String → str gives us one
let boxed = Box::new(String::from("hello"));
assert_eq!(boxed.len(), 5); // deref auto-applied for method calls too
}
}
Deep Comparison
OCaml vs Rust: Deref Coercions
Side-by-Side Code
OCaml
(* OCaml has no deref coercions — every conversion is explicit.
You cannot pass a `bytes` where a `string` is expected. *)
let greet (name : string) = Printf.printf "Hello, %s!\n" name
let () =
let s = "Alice" in
greet s; (* only string literals / string values work *)
let b = Bytes.of_string "Bob" in
greet (Bytes.to_string b) (* must convert explicitly *)
Rust (idiomatic — coercions do the work)
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn sum(data: &[i32]) -> i32 {
data.iter().sum()
}
let s: String = String::from("Alice");
greet(&s); // &String → &str (one coercion)
let boxed: Box<String> = Box::new(String::from("Bob"));
greet(&boxed); // &Box<String> → &String → &str (two coercions)
let v: Vec<i32> = vec![1, 2, 3];
sum(&v); // &Vec<i32> → &[i32] (one coercion)
Rust (custom Deref — same mechanism for user types)
use std::ops::Deref;
struct MyVec<T>(Vec<T>);
impl<T> Deref for MyVec<T> {
type Target = [T];
fn deref(&self) -> &[T] { &self.0 }
}
fn sum_my_vec(v: &MyVec<i32>) -> i32 {
sum(v) // &MyVec<i32> coerces to &[i32] via Deref
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| String borrow | string (value, GC-managed) | &str (slice reference) |
| Owned string | string (immutable) | String (heap-owned) |
| Slice borrow | 'a array | &[T] |
| Owned list | 'a list | Vec<T> |
| Smart pointer | 'a ref | Box<T>, Rc<T>, Arc<T> |
| Auto conversion | not available | Deref coercion (transitive) |
Key Insights
Bytes.to_string, string_of_int, etc. at every boundary. Rust's Deref trait lets the compiler insert conversions silently when the types are related by a deref chain.&Box<String> becomes &str in two steps — the compiler finds the shortest coercion path automatically. OCaml has no equivalent; you compose converters by hand.&str or &[T] is automatically compatible with String, Box<String>, Rc<String>, Vec<T>, Box<Vec<T>>, and any custom newtype that implements Deref. In OCaml you would need separate functions or a functor.Deref for a newtype wrapper makes it transparently usable wherever the inner type is expected — no trait objects or conversion helpers needed.When to Use Each Style
**Use idiomatic Rust (&str / &[T] parameters) when:** you want callers to freely pass any owning or borrowing form of the data — this is the normal case for library functions.
**Use custom Deref when:** you have a newtype wrapper that should behave like its inner type in most contexts (e.g., MyVec, Path, OsStr).
Exercises
print_all(items: &[impl Display]) and call it with &Vec<i32> and &[i32; 4] without any explicit conversion.LoggedVec<T> newtype that implements Deref<Target = Vec<T>> and logs every access.&Box<String> → &String → &str in a single call.