Практическая ситуация: почему ограничение обратного вызова на работу с любой длительностью заимствования выражают через for<'a>, а не через один параметр времени жизни?
for<'a> означает, что один и тот же тип обратного вызова обязан работать с ссылкой при любом времени жизни 'a. Ограничение с одним конкретным параметром времени жизни описывает только заранее выбранную связь и не гарантирует универсальность обратного вызова.
Это важно, когда функция передаёт callback временные заимствования, не позволяя callback сохранить такую ссылку или потребовать от вызывающего кода определённого срока её действия.
В Rust время жизни стало частью системы типов, чтобы безопасно использовать ссылки без сборщика мусора и без ручного управления памятью. Для простых функций достаточно связать времена жизни входных и выходных ссылок, но обобщённым API иногда нужно выразить более сильное требование: функция или замыкание должно принимать заимствование любого допустимого срока.
Именно для такого универсального случая используется higher-ranked trait bound, обычно записываемый как for<'a>. Он позволяет отделить универсальность самого обработчика от конкретного времени жизни отдельного вызова.
Представим функцию, которая передаёт callback ссылку на временный фрагмент данных. Длительность этого заимствования заранее неизвестна: она определяется конкретным вызовом и может отличаться от вызова к вызову.
Если callback рассчитан только на один конкретный 'a, он может оказаться связан с внешним заимствованием или потребовать сохранить ссылку дольше, чем это разрешено. Это либо сузит область применения API, либо приведёт к ошибке заимствования.
for<'a> Fn(&'a str) читается как: «тип реализует Fn для каждого возможного 'a». Параметр 'a выбирается при каждом вызове, поэтому callback не может заранее рассчитывать на конкретную длительность ссылки.
В этом примере apply может передать callback ссылку с любым временем жизни, допустимым в конкретном вызове. Замыкание возвращает число и не сохраняет ссылку, поэтому требование универсальности выполнимо.
Для сравнения, ограничение вида F: Fn(&'a str) связывает callback с одним параметром 'a, объявленным во внешнем контексте. Оно не выражает требование, что тот же экземпляр F пригоден для всех возможных времён жизни; for<'a> добавляет именно универсальное квантование.
HRTB не продлевает время жизни ссылки и не делает данные бессрочными. Он только проверяет, что реализация callback не зависит от конкретной длительности входного заимствования. Если callback должен вернуть ссылку, одной универсальности недостаточно: нужно отдельно описать связь времени жизни возвращаемой ссылки с входной.
Компромисс состоит в том, что HRTB делает контракт API более строгим. Это повышает безопасность и переиспользуемость callback, но замыкание, которое хранит переданную ссылку или требует конкретного внешнего заимствования, такой контракт обычно не удовлетворит.
В библиотеке обработки текстовых буферов функция принимает callback для вычисления метрики по каждому временному фрагменту. Рассматривались два варианта.
Первый вариант — привязать callback к времени жизни всего буфера. Он проще для чтения, но излишне ограничивает API: обработчик становится связан с конкретным заимствованием и потенциально может зависеть от длительно живущего внешнего объекта.
Второй вариант — потребовать for<'a> Fn(&'a str) -> R. Он гарантирует, что обработчик работает с любым фрагментом независимо от его времени жизни. Выбран второй вариант: функция не хранит фрагмент после вызова, а HRTB точно отражает это свойство и позволяет безопасно использовать один callback в разных местах.
Обязательно ли явно писать for<'a> для Fn(&str)?
Нет, в ряде позиций Rust применяет элизию времени жизни и трактует Fn(&str) как обобщённое ограничение по времени жизни аргумента. Явная форма for<'a> Fn(&'a str) полезна, когда нужно снять неоднозначность или подчеркнуть смысл контракта.
Гарантирует ли HRTB, что callback не может сохранить ссылку?
Само по себе ограничение описывает сигнатуру вызова, а не все свойства реализации. Однако если callback должен работать с любым 'a, он не может безопасно вернуть или сохранить значение, которое требует конкретного входного времени жизни, если это не отражено в типе результата или состоянии. Компилятор проверяет это через обычные правила заимствования и ограничения типов.
Чем HRTB отличается от требования 'a: 'b?
Связь 'a: 'b утверждает, что конкретное время жизни 'a охватывает 'b. Это отношение между выбранными временами жизни. for<'a> не сравнивает два времени жизни, а утверждает универсальность: описанный тип должен быть корректен для каждого возможного 'a.