0

I want a trait that is an indexable. However, its implementation may be a 'standard' array (one that actually holds the things it returns; e.g. Vec of integers) or a non-standard array that constructs the value in the index function (and so must return a value, not reference). What is the easiest way to do it? I can't use the Index trait as it won't allow the latter. Currently it looks like I will have to wrap "standard Index"-es (e.g. Vec) with a custom Index trait that returns some Self::Output (and not &Self::Output). Sounds a bit too much for such a simple abstraction that I expect to be a pretty common need.

(About lifetimes; Be it a value or a reference, I intend to use the indexed value only during the indexer is lifetime)

  • 3
    Do you really need the `[]` notation? If not, you could invent your own `Indexable` trait, with a method as `.at(position)`, implement it for any type that implement the standard `Index` trait (by using a dereference), then implement it in a totally different way for the types that should generate the result instead of accessing it. – prog-fh Jul 15 '23 at 10:15

2 Answers2

2

The v[i] operation is resolved as *v.index(i); the .index() method comes from the std::ops::Index trait. This std::ops::Index trait cannot be directly reused in your specific case in order to return a value, since it would change its semantics.

If we don't absolutely need the [] notation, then we can define our own Indexable trait dedicated to return a value at a given index.

All the types that already implement std::ops::Index can be made to automatically implement this new Indexable trait by cloning the referenced element in order to provide a value (not a reference). Of course, this only applies to containers which elements implement Clone.

Any specific type relevant for your use case could then implement the Indexable in its own manner.

All of these different implementations should be usable in a common context, giving the ability to substitute an indexable with another one.

Please find below an example for all of this.

/// A specific trait to obtain a _value_ at a given index.
trait Indexable<Idx>
where
    Idx: ?Sized,
{
    type Output: ?Sized;
    fn value_at(
        &self,
        idx: Idx,
    ) -> Self::Output;
}

/// Generic implementation of Indexable for anything that implements Index.
///
/// The stored values must be clone-able in order to provide a value
/// without consuming the container.
impl<T: ?Sized, Idx, V> Indexable<Idx> for T
where
    T: std::ops::Index<Idx, Output = V>,
    V: Clone,
{
    type Output = V;

    fn value_at(
        &self,
        idx: Idx,
    ) -> Self::Output {
        self.index(idx).clone()
    }
}

/// A specific type for the purpose of the example
struct Dummy {}

/// This implementation of Indexable for this specific type
/// produces a value instead of accessing a previously stored one.
impl Indexable<usize> for Dummy {
    type Output = f64;

    fn value_at(
        &self,
        idx: usize,
    ) -> Self::Output {
        idx as f64 * 0.1
    }
}

fn work_with_indexable<Src: Indexable<usize, Output = f64>>(
    source: &Src,
    range: std::ops::Range<usize>,
) -> f64 {
    let mut accum = 0.0;
    for i in range {
        let v = source.value_at(i);
        println!("got {} at {}", v, i);
        accum += v;
    }
    accum
}

fn main() {
    println!("~~~~ generic implementation used on a vector ~~~~");
    let v = vec!["aa".to_owned(), "bb".to_owned(), "cc".to_owned()];
    for i in 0..v.len() {
        println!("vector at {} ~~> {}", i, v.value_at(i));
    }
    println!("~~~~ generic implementation used on an array ~~~~");
    let a = ["dd".to_owned(), "ee".to_owned(), "ff".to_owned()];
    for i in 0..a.len() {
        println!("array at {} ~~> {}", i, a.value_at(i));
    }
    println!("~~~~ specific implementation used on a dedicated type ~~~~");
    let d = Dummy {};
    for i in 0..3 {
        println!("dummy at {} ~~> {}", i, d.value_at(i));
    }
    println!("~~~~ using different implementations ~~~~");
    let r1 = work_with_indexable(&[1.2, 2.3, 3.4], 0..3);
    println!("slice: {}", r1);
    let r2 = work_with_indexable(&d, 0..3);
    println!("dummy: {}", r2);
}
/*
~~~~ generic implementation used on a vector ~~~~
vector at 0 ~~> aa
vector at 1 ~~> bb
vector at 2 ~~> cc
~~~~ generic implementation used on an array ~~~~
array at 0 ~~> dd
array at 1 ~~> ee
array at 2 ~~> ff
~~~~ specific implementation used on a dedicated type ~~~~
dummy at 0 ~~> 0
dummy at 1 ~~> 0.1
dummy at 2 ~~> 0.2
~~~~ using different implementations ~~~~
got 1.2 at 0
got 2.3 at 1
got 3.4 at 2
slice: 6.9
got 0 at 0
got 0.1 at 1
got 0.2 at 2
dummy: 0.30000000000000004
*/
prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • It is a bad idea to define `Indexable` this way and always clone. Better to define it as GAT. – Chayim Friedman Jul 15 '23 at 19:38
  • @ChayimFriedman Sorry, I don't know how to do this. Maybe should you post an answer which shows the better way to handle this situation. I would be interested in seeing how it should be done; thanks. – prog-fh Jul 15 '23 at 19:43
  • I tried to construct an example but I was unable to overcome a restriction with lifetimes. I think it may be impossible to define it as GAT. – Chayim Friedman Jul 15 '23 at 19:46
  • @prog-fh I gave it a shot. Let me know if there's something I'm missing or overlooked! – BrownieInMotion Jul 16 '23 at 00:13
1

Working off of the answer provided by prog-fh, here is my attempt at writing Indexable while avoiding the clone. This lets us directly return the references when Index is implemented, but also write implementations that return values that aren't references.

trait Indexable<'a, Idx>
where
    Idx: ?Sized,
{
    type Output<'b>: ?Sized
    where
        Self: 'b,
        'a: 'b;
    fn at<'b>(&'b self, idx: Idx) -> Self::Output<'b>
    where
        'a: 'b;
}

impl<'a, T: ?Sized, Idx, V> Indexable<'a, Idx> for T
where
    T: std::ops::Index<Idx, Output = V>,
    V: 'a,
{
    type Output<'b> = &'b V where Self: 'b, 'a: 'b;
    fn at<'b>(&'b self, idx: Idx) -> Self::Output<'b>
    where
        'a: 'b,
    {
        self.index(idx)
    }
}

struct Custom {}

impl Indexable<'static, usize> for Custom {
    type Output<'a> = usize;

    fn at<'a>(&'a self, idx: usize) -> Self::Output<'a>
    where
        'static: 'a,
    {
        idx * 2
    }
}

fn main() {
    println!("testing on vector");
    let v = vec!["a".to_owned(), "b".to_owned(), "c".to_owned()];
    for i in 0..v.len() {
        println!("{} -> {}", i, *v.at(i));
    }

    println!("testing on custom");
    let c = Custom {};
    for i in 0..3 {
        println!("{} -> {}", i, c.at(i));
    }
}
BrownieInMotion
  • 1,162
  • 6
  • 11
  • Just a question: When I use it in another trait (e.g. when returning an 'Indexable' from a method) what should I do with the lifetime? Is there a way by which I'll be able not to make the the new trait itself generic? – Bipolo TheGod Jul 16 '23 at 07:58
  • @BipoloTheGod your remark here is the main problem with this approach: the semantics changes from one implementation to another. If some implementations return values while other implementations return references, how could we use all of them in a common context? An algorithm dealing with values will be different from an algorithm dealing with references. – prog-fh Jul 16 '23 at 08:10
  • @prog-fh Well, indeed it would be different in the assembly, but theoretically it could be an implementation detail that is not presented in the trait itself (easy to imagine with c++) – Bipolo TheGod Jul 16 '23 at 11:45
  • 1
    @BipoloTheGod In C++ there is no distinction in the **syntax** when using a reference or a value (the `*` is implicit in C++, but this is a detail specific to this peculiar language; I don't know any other language like this, many others use references **everywhere** so that the disctinction does not even need to be made). In C or in Rust there is a difference between using or not the dereference (`*`) operation, because this is fundamentally different in the end, and even more with Rust where lifetime considerations come into play when using references. – prog-fh Jul 16 '23 at 13:07