Search code examples
rusttraitstrait-bounds

How to specify multiple possibilities for rust trait bounds


I am trying to write a function that takes any iterable to a generic type as input and loops over the elements. Here is a working example

 pub fn test1<'a, IterableT, ItemT>(nodes: &'a IterableT)
    where 
        &'a IterableT: IntoIterator<Item = &'a ItemT>,
        ItemT: 'a + Debug
    {
        for x in nodes {
            println!("consuming or iterating: {:?}!", x);
        }
    }

This works with reference types. I struggle to implement a version that works with both regular types and references. The closest I have got it the following

 pub fn test1<IterableT>(nodes: IterableT)
    where 
        IterableT: IntoIterator,
        IterableT::Item: Debug
    {
        for x in nodes {
            println!("consuming or iterating: {:?}!", x);
        }
    }

This works as long as the reference version of the type I am using implements IntoIterator (for example vec, &vec and &mut vec). The problem is that I can't reintroduce the ItemT generic since then the underlying Item is narrowed down to one of Item or &Item

 pub fn test1<IterableT, ItemT>(nodes: IterableT)
    where 
        IterableT: IntoIterator<Item = ItemT>, // alternatively IntoIterator<Item = &ItemT>
        ItemT: Debug
    {
        for x in nodes {
            println!("consuming or iterating: {:?}!", x);
        }
    }

Depending on whether I use IntoIterator<Item = ItemT> or IntoIterator<Item = &ItemT>, this works with vec or &vec but not both. A hacky workaround is to specify IterableT::Item: Into<ItemT>, but this only works if the underlying type implements the Into traits (so this will work with built-in types like i32, i64, but not for some custom made struct).

Is there a way to indicate that Item can either be ItemT or &ItemT in the where clause? The reason I need to introduce ItemT is because it is a generic variable of the parent struct that this function is a part of. I need to accept iterators specifically with that type.


Solution

  • You can use std::borrow::Borrow to treat T and &T the same.

    use std::{borrow::Borrow, fmt::Debug};
    
    fn debug_iterator<I, T>(it: I)
    where
        I: IntoIterator,
        I::Item: Borrow<T>,
        T: Debug + ?Sized,
    {
        for item in it.into_iter() {
            println!("  item: {:?}", item.borrow());
        }
    }
    

    In action:

    fn main() {
        let v1 = vec![String::from("hello"), String::from("world")];
    
        println!("By reference:");
        debug_iterator::<_, String>(v1.iter());
        
        println!("By value:");
        debug_iterator::<_, String>(v1);
    }
    

    Output:

    By reference:
      item: "hello"
      item: "world"
    By value:
      item: "hello"
      item: "world"
    

    Note that because the Borrow trait has a type parameter, you have to provide the correct type for T explicitly at the call site; Rust will not be able to infer the type you want.