Практическая ситуация: API должен принимать любой тип с трейтом Readable, кроме типов с признаком Encrypted. Почему такой bound нельзя выразить напрямую в Rust?
Rust не поддерживает универсальные отрицательные bounds вида «T реализует Readable, но не реализует Encrypted». Bound описывают требуемые возможности типа, а не запреты на другие реализации. Поэтому ограничение нужно выразить положительно: ввести отдельный capability-трейт, обёртку-новый тип или закрытый трейт.
Система трейтов Rust строится вокруг открытого мира: реализация трейта может появиться в другом модуле или крейте, если это допускают правила согласованности. Компилятор должен проверять корректность generic-кода без предположения, что перечисленные сегодня реализации являются полным набором.
Отрицательный bound потребовал бы рассуждать об отсутствии реализации, которая может появиться позднее. Это усложнило бы разрешение реализаций, проверку пересечений blanket-реализаций и стабильность библиотечных API.
Предположим, библиотека хочет обработать все Readable-типы, кроме Encrypted. Проверка только T: Readable пропустит запрещённый тип, а попытка добавить условие «не Encrypted» не имеет общего синтаксиса и семантики в stable Rust.
Нельзя надёжно решить задачу перечислением известных исключений: внешний крейт может добавить новую реализацию, а сам тип может быть обобщённым. Нельзя также создать blanket-реализацию для всех Readable с последующим исключением: такие реализации пересекаются, а Rust не поддерживает специализацию как универсальный способ разрешать этот конфликт.
Обычно отрицательное требование заменяют положительной моделью возможностей:
Теперь process принимает только типы, которым явно разрешено участвовать в безопасном API. Encrypted может реализовать Readable, но без SafeReadable он не удовлетворяет bound.
Другой вариант — newtype: API принимает специальную обёртку, создать которую можно только после явной проверки. Это даёт сильную границу безопасности, но требует преобразования значения и иногда добавляет неудобство при работе с существующими типами.
Если набор допустимых типов должен контролироваться библиотекой, подходит sealed trait: публичный трейт наследуется от закрытого внутреннего трейта, поэтому внешние крейты не смогут произвольно добавить реализации. Цена — меньшая расширяемость API.
Отрицательные реализации, где они доступны для отдельных механизмов языка, не превращаются в универсальный отрицательный bound для произвольного трейта. Они не позволяют написать общий API с условием «T не реализует Encrypted».
Библиотека чтения файлов хотела автоматически принимать любой тип, реализующий Readable, но исключить типы, содержащие зашифрованные данные. Вариант с гипотетическим отрицательным bound был бы удобен, но недоступен и плохо сочетается с открытым расширением трейтов.
Рассматривались два решения. Blanket-реализация SafeReadable для всех Readable была бы простой, но не позволила бы надёжно исключить отдельный тип из-за пересечения реализаций. Newtype обеспечил бы строгий контроль, но заставил бы пользователей оборачивать значения.
Выбрали отдельный capability-трейт SafeReadable, реализации которого выдаёт библиотека после проверки типа. В результате generic-функция получает положительную и проверяемую гарантию, а запрещённые типы не проходят компиляцию без специальных исключений.
where-условии?Нет. Запись нескольких положительных bound только требует одновременного наличия соответствующих реализаций. Она не меняет смысл bound на отрицательный и не создаёт приоритет между конфликтующими реализациями.
Readable, а потом переопределить его для Encrypted?Такие реализации пересекаются для Encrypted: общий blanket-impl уже покрывает этот тип. Без гарантированного механизма специализации компилятор не может выбрать, какая реализация должна иметь приоритет. Это ограничение защищает согласованность и предсказуемость разрешения трейтов.
Capability-трейт подходит, когда нужно выразить право типа участвовать в API и допустимо явно перечислять разрешённые реализации. Newtype лучше, когда гарантия должна возникать только после контролируемого преобразования или когда исходный тип нельзя изменять. Capability-трейт удобнее для generic-кода, а newtype обычно даёт более строгую границу инварианта.