Почему данный код не компилится, или попытки постичь borrow checker

Я тут воюю с borrow checker’ом…
Обращаю внимание - цель данного вопроса не в том, чтобы заставить это компилиться. а в том, чтобы понять, что происходит. То есть впринципе, мне понятно - неверно расставлено время жизни. Но почему такое время жизни неверно? Если убрать время жизни, то этот код откомпилится. Компилятор сам выведет времена жизни. То есть фактически, вопрос в том, как их расставляет компилятор.

use std::collections::HashMap;

trait HasName{
    fn print_name(&self);
}

fn foo<'a>(map: &'a mut HashMap<i32, Box<HasName+ 'a>>, k: i32){
}

fn main() {
    let mut map:HashMap<i32, Box<HasName>> = HashMap::new();
    foo(&mut map, 1);
}

Мне как бы здравый смысл подсказывает, что ссылка на мапу не может жить меньше, чем живет структура. Вот в коде выше я это и указал - как у мапы, так и у структуры время жизни 'a. Но это не прокатывает. Я не пойму - почему? Ведь по определению, когда мапа умирает - то она вызывает умирание HasName. То есть (согласно моему пониманию), время жизни получается одинаковое.

Ладно… Дадим каждой сущности свое время жизни.
Сигнатура получается такой:

fn foo<'a, 'b>(map: &'a mut HashMap<i32, Box<HasName+ 'b>>, k: i32) 

В этом случае, компиляция и прокатывает - но я снова не пойму почему. Ведь нигде не указано, что 'a - более долгое время жизни, чем 'b. А вдруг я в эту функцию подам объекты, у которых время жизни будет перепутано?
Это навело меня на мысль, что времена жизни перечисляются в порядке от большего к меньшему. Проверим это, поменяв местами 'a с 'b:

fn foo<'b, 'a>....

Результат - все компилится. То есть получается, чекеру не важно какое время жизни больше, а какое меньше. Но когда они равны (как в первом примере) - это получается ошибка. Очень странное поведение. Тем более, что у нас де-факто они и есть равны: 'a=='b, с точностью до порядка создания и освобождения ресурсов.

На самом деле borrowchecker - вещь довольно тупая и действует по принципу “лучше перебдеть…” :sob:
Границы времени жизни
Разработчики планируют конечно улучшить его работу: Non-lexical lifetimes: introduction, но это дело движется не очень быстро.

В вашем примере, судя по всему, map заимствует саму себя:

error: `map` does not live long enough
  --> <anon>:13:1
   |
12 |     foo(&mut map, 1);
   |              --- borrow occurs here
13 | }
   | ^ `map` dropped here while still borrowed
   |

Тут просто нет других вариантов - функция получает одну ссылку и ничего не возвращает.

Если времена жизни разные то компилятор, видимо понимает, что ссылка на map не связана с ее содержимым.

Это навело меня на мысль, что времена жизни перечисляются в порядке от большего к меньшему.

Это не так. Кто кого переживет можно задать в явном виде:

fn foo<'a,'b:'a>(map: &'a mut HashMap<i32, Box<HasName+ 'b>>, k: i32)

'b:'a - означает что 'b включает 'a или 'b больше чем 'a . Subtyping and Variance

В большинстве случаев этого можно не делать т.к. Раст неплохо сам выводит эти зависимости.
Так работать не будет:

fn foo<'b,'a:'b>(map: &'a mut HashMap<i32, Box<HasName+ 'b>>, k: i32)

Интересно, что сообщение об ошибке в этом случае точно такое же как для случая с одним временем жизни.

Если убрать время жизни, то этот код откомпилится. Компилятор сам выведет времена жизни. То есть фактически, вопрос в том, как их расставляет компилятор.

Это легко проверить:

fn foo(map: &mut HashMap<i32, Box<HasName>>, k: i32){
    let _:() = map;
}

Получим:

 = note: expected type `()`
             found type `&mut std::collections::HashMap<i32, std::boxed::Box<HasName + 'static>>`
2 лайка

Хочу пояснить один момент по поводу равных времен жизни.

То что у разных ссылок в сигнатуре функции проставлено одно время жизни не означает что у исходных объектов оно одно. (Вообще сомневаюсь что такое возможно если вж не 'static)
Это означает что внутри функции это вж будет считаться равным пересечению (или наименьшему из) исходных вж.

Например:

fn test<'t>(x:&'t str,_y:&'t str) -> &'t str{
    x
}

fn main() {
    let x = "static str";
    let z = {
        let y = "local string".to_owned();
        test(x, &y)
    };
    println!("{}", z);
}

Мы передаем в функцию test() два аргумента: &'static str и &str с неким локальным вж. Функция возвращает первый аргумент (&'static str) но время жизни у результата уже не 'static.
Еще один побочный эффект - компилятор считает что результат заимствует оба аргумента, хотя по факту это не так.

2 лайка