Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a generic function that takes an iterable container as parameter in Rust

I want to write a generic function that takes any immutably borrowed iterable container such as an array, Vec, BTreeSet, etc. Since this function is part of a trait that I am implementing, I am not able to change the signature of it, so it's not possible to directly take an iterator as parameter and I also can't introduce any lifetime parameters to the function signature.

Context

I tried to implement the observer pattern in Rust. The observable and the observer look as follows:

struct Observable<T> {
    value: T,
}

impl<T> Observable<T> {
    pub fn get(&self) -> &T {
        &self.value
    }
}

trait Observer<T> {
    fn update(&self, &Observable<T>);
}

(Some functions that were irrelevant to my problem are omitted)

It is now my objective to write an observer that can be used with arbitrary iterable containers which hold items that can be assigned a value. It is supposed to keep track of the sum of values of the items in the container and therefore holds the current sum and a function that calculates the value of any item. It should implement the Observer trait so the sum can be updated each time the container changes.

use std::cell::RefCell;

struct SumObserver<T> {
    current_sum: RefCell<i64>,
    get_value: Fn(&T) -> i64,
}

Approaches so far

I have unsuccessfully tried to get the update function to compile for quite some time. The following is one of the versions of the function that I tried:

impl<'a, T, L> Observer<L> for SumObserver<T>
where
    &'a L: IntoIterator<Item = &'a T>,
{
    fn update(&self, observable: &Observable<L>) {
        let mut sum: i64 = 0;
        for item in observable.get() {
            sum += (self.get_value)(item);
        }
        *self.current_sum.borrow_mut() = sum;
    }
}

However, the compiler complains that both parameter types T and L might not live long enough:

error[E0309]: the parameter type `T` may not live long enough
  --> src/lib.rs:22:1
   |
22 |   impl<'a, T, L> Observer<L> for SumObserver<T>
   |   ^        - help: consider adding an explicit lifetime bound `T: 'a`...
   |  _|
   | |
23 | | where
24 | |     &'a L: IntoIterator<Item = &'a T>,
25 | | {
...  |
32 | |     }
33 | | }
   | |_^
   |
note: ...so that the reference type `&'a T` does not outlive the data it points at
  --> src/lib.rs:22:1
   |
22 | / impl<'a, T, L> Observer<L> for SumObserver<T>
23 | | where
24 | |     &'a L: IntoIterator<Item = &'a T>,
25 | | {
...  |
32 | |     }
33 | | }
   | |_^

The error message even stays the same if the whole function body is commented out. If I also remove the where-clause, the compilation works.

If I follow the compiler's suggestion to add explicit lifetime bounds to the parameter types:

impl<'a, T: 'a, L: 'a> Observer<L> for SumObserver<T>

The compiler gives the following error:

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/lib.rs:28:32
   |
28 |         for item in observable.get() {
   |                                ^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the method body at 26:5...
  --> src/lib.rs:26:5
   |
26 | /     fn update(&self, observable: &Observable<L>) {
27 | |         let mut sum: i64 = 0;
28 | |         for item in observable.get() {
29 | |             sum += (self.get_value)(item);
30 | |         }
31 | |         *self.current_sum.borrow_mut() = sum;
32 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:28:21
   |
28 |         for item in observable.get() {
   |                     ^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the impl at 22:6...
  --> src/lib.rs:22:6
   |
22 | impl<'a, T: 'a, L: 'a> Observer<L> for SumObserver<T>
   |      ^^
   = note: ...so that the types are compatible:
           expected std::iter::IntoIterator
              found std::iter::IntoIterator

I don't understand the problem with lifetimes in this function. At any point where this function is called, the compiler should make sure that the borrow of observable lasts at least until the function returns. At that time, any borrow of observable has gone out of scope.

like image 430
Zoidberg Avatar asked Mar 11 '16 12:03

Zoidberg


1 Answers

This is a case for Higher Ranked Trait Bounds (HRTB).

The point is that you do not want &L to implement IntoIterator<Item = &T> for one lifetime but for all potential lifetimes that L may happen to have.

In this case, you need to use a Higher Ranked Trait Bound: for<'a> will take care of introducing the lifetime name whilst simultaneously signaling to the compiler that the clause using it should be valid for all possible values of 'a.

This means:

impl<T, L> Observer<L> for SumObserver<T>
where
    for<'a> &'a L: IntoIterator<Item = &'a T>,
{
    fn update(&self, observable: &Observable<L>) {
        let mut sum: i64 = 0;
        for item in observable.get() {
            sum += (self.get_value)(item);
        }
        *self.current_sum.borrow_mut() = sum;
    }
}

which compiles (at least in isolation).

See also:

  • How does for<> syntax differ from a regular lifetime bound?
like image 78
Matthieu M. Avatar answered Oct 20 '22 17:10

Matthieu M.