How to delegate from a thin collection wrapper?

For a thin collection wrapper, is the best way to delegate to the contained collection's methods to wrap them all, or is there a better way?

At the moment I have these:

pub type Row = Vec<Value>;

#[derive(Clone, Debug)]
pub struct List {
    vtype: String,
    comment: String,
    values: Row,
}

impl List {
    pub fn new(vtype: &str, comment: &str) -> Result<Self> {
        if !vtype.is_empty() {
            check_name(vtype)?;
        }
        Ok(List {
            vtype: vtype.to_string(),
            comment: comment.to_string(),
            values: Row::new(),
        })
    }

    pub fn vtype(&self) -> &str { &self.vtype }
    pub fn comment(&self) -> &str { &self.comment }
    pub fn len(&self) -> usize { self.values.len() }
    pub fn is_empty(&self) -> bool { self.values.is_empty() }
    pub fn get(&self, index: usize) -> Option<&Value> { self.values.get(index) }
    pub fn get_mut(&mut self, index: usize) -> Option<&mut Value> { self.values.get_mut(index) }
    pub fn get_unchecked(&self, index: usize) -> &Value { &self.values[index] }

    // Wrap the rest of Vec's API?


#[derive(Clone, Debug)]
pub struct Map {
    ktype: String,
    vtype: String,
    comment: String,
    items: HashMap<Key, Value>,
}

impl Map {
    pub fn new(ktype: &str, vtype: &str, comment: &str) -> Result<Self> {
        if !ktype.is_empty() {
            check_ktype(ktype)?;
        }
        if !vtype.is_empty() {
            check_name(vtype)?;
        }
        Ok(Map {
            ktype: ktype.to_string(),
            vtype: vtype.to_string(),
            comment: comment.to_string(),
            items: HashMap::new(),
        })
    }

    // Wrap the rest of HashMap's API?

Note that ktype and vtype are read-only properties set once at construction time, but the values or items are mutable.

Assuming you’d be okay with fully exposing the contained Vec/HashMap (so that users of your API can e.g. obtain a &mut Row or a &mut HashMap<Key, Value>), there’s the option of using the Deref+DerefMut trait for achieving behavior similar to delegating all of Vec’s or HashMap methods. Well, at least all of the &self and &mut self methods, not the constructors, or into_iter [1], etc… you’ll see yourself, feel free to given an impl Deref for List { type Target = Row; … } impl DerefMut for List { … } a try and see whether or not if fits your use-case.


  1. So you’d need to implement the IntoIterator trait yourself; for convenience maybe also for &List and &mut List, delegating to .iter() and .iter_mut() accordingly. ↩︎

1 Like

I did think about using Deref, but the docs say:

"Because of this, Deref should only be implemented for smart pointers"

and neither List nor Map is a smart pointer (at least not explicitly), but maybe Vec and HashMap are? Or is this an acceptable use case anyway?

Deref and DerefMut aren't supposed to be used for imitating inheritance. But you don't need them; you can just expose the inner data structures via normal inherent methods.

Do you mean provide some kind of inner method...

pub fn inner(&self) -> &Row {
    &self.values
}
pub fn inner_mut(&mut self) -> &mut Row {
    &mut self.values
}

That wouldn't be nice for users:

let mut lst = List::default();
lst.push(Value::Int(5));
// vs
lst.inner_mut().push(Value::Int(5));

Personnally, that's what I would do (it's what I have done in the past). But using the name as_inner[_mut]. The borrowing rules makes APIs like that feasible while they wouldn't be in other languages (other languages could keep references alive for longer than you'd want).

Yes, exactly that.

Why?

It isn't as nice because:

  1. users have to be aware of inner which is really just an implementation detail;
  2. users have to type more.

That's why using Deref is so attractive. But at the same time I don't want to go against what the docs recommend, so ISTM that wrapping as much of the API as is needed is the least worst solution.

later...

In the end I opted to provide wrappers for the most commonly needed methods (e.g., get, get_mut, [], push, truncate, clear) and inner and inner_mut to make the whole Vec API available.

I really think that it would be nice if Rust offered some solution to this. I realise that anything inheritance-like brings with it the risk of method name clashes etc.

Anyway, one kind of idea might be something along the lines of:

#[derive(Clone, Debug)]
pub struct List {
    vtype: String,
    comment: String,
    values: Vec<Value>,
}

impl List {
    // new and getter/setter for vtype & comment & other List-specific methods
}

impl Delegate for List {
    type inner = values;
}

The Delegate would mean, that given lst of type List when the compiler looks to resolve a method, say, lst.method(), if method() isn't in impl List, then since List is a Delegate, the compiler would then try lst.inner.method() where inner is values, i.e., lst.values.method(). This would at least have the virtue of hiding an implementation detail; but I guess it isn't practical.

Both .inner_mut() and DerefMut expose the implementation equally. They both

  • answer the question "what operations are available?" with "see HashMap", and
  • obligate the implementation to use a HashMap and not change to anything else without making a breaking API change.