dyn-Compatibility
What is dyn-Compatibility?
- In Rust, when you have
Box<dyn State>, you are creating a trait object. This allows you to hold different concrete types (like SO2State, SO3State, etc.) in the same container (like the Vec in CompoundState), as long as they all implement the State trait. This is a form of runtime polymorphism. - For a trait to be usable as a trait object (i.e., to be "
dyn-compatible" or "object-safe"), the compiler must be able to work with it without knowing the concrete type at compile time.
Why a Method Returning Self is a Problem
- The issue arises with methods that return
Self, like the standard clone method:
pub trait Clone {
fn clone(&self) -> Self; // This returns the concrete type itself.
}
- Imagine you have a
Box<dyn State> and you call .clone() on it. The compiler knows it's some kind of State, but it doesn't know which one. Is it an SO2State or a CompoundState? - If it's an
SO2State, the clone method needs to return a new SO2State. - If it's a
CompoundState, it needs to return a new CompoundState.
- These types can have different sizes in memory. Since the compiler doesn't know the concrete type at runtime, it doesn't know how much memory to allocate for the return value. This ambiguity makes any trait with a
-> Self method "object-unsafe."
Solution
- Create a New Trait (
DynClone): A new trait DynClone is created. - Define an Object-Safe Method: Instead of returning
Self, its method clone_box returns a Box<dyn State>. A Box is a smart pointer, and the size of the pointer itself is known at compile time, regardless of what it points to. This makes the method object-safe.
pub trait DynClone {
fn clone_box(&self) -> Box<dyn State>; // Returns a Box, which has a known size.
}
Downcasting
The Problem
- Imagine your
distance_dyn function is a party host. It receives two guests: state1: &dyn State and state2: &dyn State
- These guests are wearing masks. The mask is the
&dyn State trait object. Because they are wearing the "State" mask, you know they can do all the things a State can do (like clone_box). However, you don't know their real identity. You don't know if state1 is a RealVectorState, an SO2State, or some other state type. - Now, imagine the
self.distance method is a special room at the party. This room has a very strict bouncer at the door. The RealVectorStateSpace's version of this room only allows &RealVectorState guests inside. - This is the core problem: How do you get your generic "masked guest" (
&dyn State) into the room that only accepts a specific type (&RealVectorState)?
The Solution
- This is where your line of code comes in. It's the process of "unmasking" the guest.
- Let's deconstruct it:
(state1 as &dyn Any).downcast_ref::<T::StateType>().unwrap();
Step 1: state1 as &dyn Any — The Prerequisite
- What it does: This is the first and most crucial step. You take your
&dyn State and convert it into a &dyn Any. - Why:
Any is a special, built-in Rust trait that all 'static types have. Its entire purpose is to enable runtime type identification. Think of this as taking your masked guest to a special "ID check" station. You can't ask for their ID out on the dance floor; you have to take them to the right place first.
Step 2: .downcast_ref::<T::StateType>() — The "Unmasking"
- What it does: This is the actual downcasting. You call the
downcast_ref method on your &dyn Any object. - The Question: You are asking the Rust runtime a direct question: "Is the real, concrete type hidden behind this
&dyn Any object a T::StateType?" -- In the context of RealVectorStateSpace, T::StateType would be RealVectorState. - The Return Value: This method returns an
Option. Some(&RealVectorState): If you were right, and the guest is indeed a RealVectorState, you get a Some containing a correctly typed reference to it. None: If you were wrong, and the guest was actually an SO2State, you get None.
Step 3: .unwrap() — Assuming You're Correct
- What it does: The
.unwrap() method on an Option does one of two things: - If the
Option is Some(value), it gives you value. - If the
Option is None, it panics.
- Why it's okay here: In your
CompoundStateSpace, you construct the CompoundState by putting a RealVectorState in the first component and an SO2State in the second. When you later call distance_dyn on your RealVectorStateSpace (the first subspace), you are guaranteed by your own logic that you are passing it the first component of the state, which you know is a RealVectorState. You are essentially telling the compiler, "I know what I'm doing. This conversion will not fail."
Putting It All Together
- So, when your
CompoundStateSpace calls this line: let component_dist = self.subspaces[i].distance_dyn(&*state1.components[i], &*state2.components[i]);
- Here's the full sequence of events for the first subspace (
i=0): - The
CompoundStateSpace passes two "masked" guests (&dyn State) to the RealVectorStateSpace's distance_dyn method. - Inside
distance_dyn, the method takes the first guest to the ID check station (as &dyn Any). - It asks the runtime, "Is this guest a
RealVectorState?" <RealVectorState>(). - The runtime says, "Yes, it is." The
downcast_ref returns Some(&RealVectorState). .unwrap() extracts the &RealVectorState from the Some. - The method now has the correctly typed reference it needs to call the real
self.distance method, and the bouncer lets the guest into the room.
- This process of downcasting is what makes your
CompoundStateSpace possible. It's the essential bridge that allows you to store generic trait objects together but still call their specific, concrete methods when needed.
Code
// In oxmpl/src/base/state.rs
use std::any::Any;
pub trait DynClone {
fn clone_box(&self) -> Box<dyn State>;
}
impl<T> DynClone for T
where
T: State + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn State> {
Box::new(self.clone())
}
}
/// A marker trait for all state types in the planning library.
///
/// A `State` represents a single point, configuration, or snapshot of the system
/// being planned for.
///
/// Supertrait bounds:
/// - `DynClone`: States must be copyable as Dyn for runtime polymorphism.
///
/// > [!NOTE] (for self)
/// > A trait is not dyn-compatible if any of its methods return Self — unless it has a `where Self: Sized` bound.
pub trait State: DynClone + Any {}
impl Clone for Box<dyn State> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Clone)]
pub struct CompoundState {
pub components: Vec<Box<dyn State>>,
}
impl State for CompoundState {}
// compound_state_space.rs
use rand::Rng;
use std::any::Any;
use crate::base::{
error::{StateSamplingError},
space::StateSpace,
state::{CompoundState, State},
};
/// A helper trait to allow calling methods on a `Box<dyn StateSpace>` with `&dyn State`.
/// This avoids needing to know the concrete `StateType` at compile time.
pub trait AnyStateSpace {
/// A version of `distance` that works with trait objects.
fn distance_dyn(&self, state1: &dyn State, state2: &dyn State) -> f64;
/// A version of `interpolate` that works with trait objects.
fn interpolate_dyn(&self, from: &dyn State, to: &dyn State, t: f64, state: &mut dyn State);
// Add other _dyn methods for enforce_bounds, satisfies_bounds, etc.
}
impl<T: StateSpace> AnyStateSpace for T
where
// The state type must be 'static to be downcast safely.
T::StateType: 'static,
{
fn distance_dyn(&self, state1: &dyn State, state2: &dyn State) -> f64 {
let s1 = (state1 as &dyn Any).downcast_ref::<T::StateType>().unwrap();
let s2 = (state2 as &dyn Any).downcast_ref::<T::StateType>().unwrap();
self.distance(s1, s2)
}
fn interpolate_dyn(&self, from: &dyn State, to: &dyn State, t: f64, state: &mut dyn State) {
let from_s = (from as &dyn Any).downcast_ref::<T::StateType>().unwrap();
let to_s = (to as &dyn Any).downcast_ref::<T::StateType>().unwrap();
let state_s = (state as &mut dyn Any).downcast_mut::<T::StateType>().unwrap();
self.interpolate(from_s, to_s, t, state_s);
}
}
/// A state space that is composed of multiple other state spaces.
///
/// This is used to represent complex configuration spaces which combine translational and
/// rotational components, etc.
pub struct CompoundStateSpace {
/// The component state spaces. `AnyStateSpace` is used for type erasure.
pub subspaces: Vec<Box<dyn AnyStateSpace + Send + Sync>>,
/// The weight of each component's contribution to the total distance.
pub weights: Vec<f64>,
}
impl CompoundStateSpace {
/// Creates a new `CompoundStateSpace`.
///
/// The `weights` vector must have the same length as the `subspaces` vector.
pub fn new(subspaces: Vec<Box<dyn AnyStateSpace + Send + Sync>>, weights: Vec<f64>) -> Self {
assert_eq!(subspaces.len(), weights.len(), "Number of subspaces must match number of weights.");
Self { subspaces, weights }
}
}
impl StateSpace for CompoundStateSpace {
type StateType = CompoundState;
fn distance(&self, state1: &Self::StateType, state2: &Self::StateType) -> f64 {
let mut total_dist_sq = 0.0;
for i in 0..self.subspaces.len() {
let component_dist = self.subspaces[i].distance_dyn(
&*state1.components[i], // Deref Box<dyn State> to &dyn State
&*state2.components[i],
);
total_dist_sq += (component_dist * self.weights[i]).powi(2);
}
total_dist_sq.sqrt()
}
fn interpolate(
&self,
from: &Self::StateType,
to: &Self::StateType,
t: f64,
out_state: &mut Self::StateType,
) {
for i in 0..self.subspaces.len() {
self.subspaces[i].interpolate_dyn(
&*from.components[i],
&*to.components[i],
t,
&mut *out_state.components[i], // Mutable deref
);
}
}
// Implement the other StateSpace methods by delegating to subspaces...
fn enforce_bounds(&self, _state: &mut Self::StateType) { todo!() }
fn satisfies_bounds(&self, _state: &Self::StateType) -> bool { todo!() }
fn sample_uniform(&self, _rng: &mut impl Rng) -> Result<Self::StateType, StateSamplingError> { todo!() }
fn get_longest_valid_segment_length(&self) -> f64 { todo!() }
}