Зачем использовать Result<T, Infallible> в обобщённом API, если конкретная операция не может завершиться ошибкой?
Result<T, Infallible> позволяет сохранить единый интерфейс обобщённой операции, хотя для конкретной реализации ошибка логически невозможна. Тип Infallible не имеет ни одного значения, поэтому ветка Err не может возникнуть: результат безопасно сводится к T через исчерпывающий разбор.
В Rust обычные ошибки моделируются явно через Result, а не через исключения или неявные значения-сигналы. Для обобщённых API это создаёт практическую задачу: одна реализация может быть fallible, а другая — гарантированно успешной.
Infallible решает её без введения отдельного интерфейса для «всегда успешных» операций. Тип операции остаётся одинаковым, а невозможность ошибки выражается системой типов.
Если для каждой реализации объявлять отдельный тип результата, обобщённый код будет вынужден иметь разные ветви обработки. Это усложняет композицию, увеличивает число адаптеров и повышает риск случайно обработать невозможную ошибку как обычный сбой.
Если же использовать Result<T, String> или другой искусственный тип ошибки, контракт становится менее точным: вызывающий код получает видимость, что ошибка возможна, хотя это не так. Кроме того, появляется соблазн возвращать фиктивные сообщения об ошибках.
Infallible — пустой тип: создать значение этого типа невозможно. Поэтому у Result<T, Infallible> фактически остаётся только вариант Ok(T), но сам Result сохраняет совместимость с обобщёнными функциями.
Во внутреннем разборе Err(error) требуется обработать синтаксически, но вложенный match является исчерпывающим: у Infallible нет вариантов. Это не проверка во время выполнения и не обработка неожиданного сбоя, а доказательство невозможности ветки на уровне типов.
Такой результат особенно полезен в generic-коде, где общий алгоритм работает с Result<T, E>, но некоторые конкретные операции не имеют реальных причин отказа. При этом Result<T, Infallible> не означает, что операция никогда не вызовет panic: паника и возвращаемая ошибка — разные механизмы.
Компромисс состоит в том, что публичный API может стать менее очевидным для простого сценария: пользователю приходится видеть Result, хотя ошибки нет. Поэтому Infallible оправдан прежде всего при унификации нескольких реализаций, а не как замена обычному T в каждой функции.
Оптимизатор обычно может устранить недостижимую ветку, но полагаться на конкретное машинное представление не следует. Гарантия здесь семантическая: значение Err нельзя корректно создать средствами безопасного Rust.
Предположим, библиотека обрабатывает последовательность преобразований. Один преобразователь проверяет формат и возвращает Result<Item, ParseError>, а другой только меняет представление уже проверенного значения и не способен завершиться ошибкой.
Можно объявить для второго преобразователя обычный Item. Это проще локально, но общий конвейер придётся специально адаптировать: часть шагов будет возвращать Item, а часть — Result.
Можно использовать фиктивную ошибку, например строку. Такой вариант сохраняет единый тип, но плохо описывает контракт и допускает бессмысленное формирование ошибки.
Выбранное решение — вернуть из безопасного преобразователя Result<Item, Infallible>. Общий конвейер остаётся единообразным, а тип явно сообщает, что именно этот шаг не может завершиться ошибкой. На границе API результат можно развернуть в Item, когда обобщённость больше не нужна.
Вопрос 1. Почему для невозможной ошибки нельзя просто использовать ()?
() — это вполне существующий тип с единственным значением. Значит, Result<T, ()> допускает вариант Err(()), и система типов не знает, что он невозможен. Infallible отличается отсутствием значений, поэтому невозможность ошибки проверяется компилятором, а не поддерживается соглашением команды.
Вопрос 2. Означает ли Result<T, Infallible>, что функция полностью защищена от аварийного завершения?
Нет. Тип описывает только возвращаемый канал ошибки. Функция всё ещё может вызвать panic!, получить переполнение или аварийно завершиться по другой причине, зависящей от режима сборки и конкретной операции. Infallible гарантирует лишь невозможность возврата именно значения Err с этим типом ошибки.
Вопрос 3. Когда лучше вернуть обычный T, а не Result<T, Infallible>?
Если функция используется самостоятельно и её успешность не нужно согласовывать с другими fallible-операциями, обычный T проще и яснее. Result<T, Infallible> имеет смысл, когда важна совместимость с обобщённым интерфейсом, единый конвейер или возможность подставить реализацию в структуру, ожидающую Result<T, E>. Иначе формальный единый тип может усложнить API без практической пользы.