Вам не нужны bool и Option

Я давно хотел написать про bool и Option, но последнее время сомневаюсь, насколько кому-то нужны такие статьи. Когда код пишет агент, happy path работает и задачу уже можно закрывать, обсуждение типов кажется необязательным. Да и код руками пишет всё меньше людей, я, в том числе. Кто-то даже уже почти не смотрит на то, что сгенерировал агент. Я пока так не «преисполнился», мне всё ещё важны читаемость, тестируемость и корректность, хотя бы на каком-то минимальном уровне паршивости.
Когда в программе есть bool и Option, я и болванчик не сразу понимаем, что именно они означают. Приходится читать окружающий код и выяснять, о каком режиме или состоянии идет речь. Я хочу видеть эти названия прямо в типах, а недопустимые сочетания значений исключить по построению.
Коротко о типах
Примеры будут на Rust. bool принимает два значения: true или false. Option<T> содержит значение типа T в варианте Some(value) или обозначает его отсутствие вариантом None.
bool в аргументах
Смысл true и false приходится узнавать из сигнатуры. Несколько булевых аргументов можно перепутать местами, и никто ничего про это не скажет.
Сходить в определение функции я, конечно, могу. Но при чтении диффа хочется проверять логику задачи. bool в этом не помогает:
fn start_job(path: &Path, refresh_cache: bool, notify: bool) {
/* ... */
}
start_job(path, true, false);Назовём режимы отдельными типами:
enum CachePolicy { Reuse, Refresh }
enum NotificationPolicy { Silent, OnCompletion }
fn start_job(path: &Path, cache: CachePolicy, notify: NotificationPolicy) {
/* ... */
}
start_job(path, CachePolicy::Refresh, NotificationPolicy::Silent);Теперь видно: запускаем задачу с обновлением кеша и без уведомления. Перестановка аргументов даст ошибку типов. Оба enum имеют по два варианта: предметные имена полезны уже здесь.
У этой проблемы есть название boolean trap. Для Rust такую же рекомендацию дают Rust API Guidelines.
Состояние описано флагами
Флаги могут просто кодировать состояние. Два bool дают четыре сочетания. Если все четыре допустимы, по набору состояний модель ничего не теряет. У настроек кеша и уведомлений выше как раз допустимы все сочетания. Проигрыш когнитивный: нужно помнить смысл каждой комбинации. Но с флагами есть ещё две проблемы.
1. Лишние сочетания флагов
Проблема появляется, когда состояний меньше, чем сочетаний флагов. Если для трёх состояний взяли два флага, одно сочетание уже лишнее. Пока флаги можно произвольно создавать и менять, обработчики должны проверять сочетание или полагаться на проверку до вызова.
Например, запущенная попытка обработки файла либо ещё выполняется, либо завершилась успехом, либо ошибкой. Одновременные успех и ошибка не предусмотрены, но такая структура позволяет их записать:
struct AttemptState {
succeeded: bool,
failed: bool,
}
fn show_status(state: &AttemptState) -> Result<(), InvalidAttemptState> {
if state.succeeded && state.failed {
return Err(InvalidAttemptState);
}
/* ... */
Ok(())
}Можно закрыть поля и проверять флаги в конструкторе. Тогда методы изменения тоже должны сохранять это правило. Лучше перечислять допустимые состояния явно:
enum AttemptState {
Running,
Succeeded,
Failed,
}Из четырёх сочетаний флагов остались три нужных состояния. Вместо двух ложных флагов теперь Running, а одновременных успеха и ошибки в типе нет.
У Option<bool> три значения: None, Some(false), Some(true). Но смысл каждого исхода всё равно придётся объяснять отдельно.
2. Данные конкретного состояния
Если конкретному состоянию нужны свои данные, одни флаги тоже не задают эту связь. Придётся отдельно проверять, что данные есть в нужном состоянии.
Добавим путь к готовому файлу, обязательный при успехе. Если валидацию не дополнить проверкой нового поля, она продолжит пропускать успех без пути:
struct AttemptState {
succeeded: bool,
failed: bool,
output: Option<PathBuf>,
}
fn validate(state: &AttemptState) -> Result<(), InvalidAttemptState> {
if state.succeeded && state.failed {
return Err(InvalidAttemptState);
}
Ok(())
}
let state = AttemptState { succeeded: true, failed: false, output: None };
assert!(validate(&state).is_ok()); // проверка прошла, хотя пути нетСвяжем путь с успешным исходом:
enum AttemptState {
Running,
Succeeded { output: PathBuf },
Failed,
}
let state = AttemptState::Succeeded {
output: PathBuf::from("report.pdf"),
};При создании Succeeded путь теперь обязателен: забыть поле не даст компилятор.
let state = AttemptState::Succeeded {}; // ошибка: отсутствует поле `output`Но использовать эти данные по назначению он не заставляет.
Когда одного None недостаточно
None обозначает отсутствие значения. Но по нему не всегда понятно, что программа должна сделать. None должен иметь однозначный смысл в контексте использования.
Например, оформляем заказ. У покупателя есть сохранённый адрес, но при оформлении можно передать другой:
// Типы Cart и Address здесь опущены.
fn checkout(cart: Cart, delivery_address: Option<Address>) {
/* ... */
}
checkout(cart, None);None здесь можно прочитать как «самовывоз» или «доставка на сохранённый адрес». По одному вызову не совсем понятно, какая трактовка выбрана.
В этой функции None означает самовывоз. Назовём этот режим:
enum Delivery {
Pickup,
ToAddress(Address),
}
fn checkout(cart: Cart, delivery: Delivery) {
/* ... */
}
checkout(cart, Delivery::Pickup);У типа те же два варианта, но в вызове теперь явно выбран самовывоз. Компилятор не помешает передать Pickup, когда нужна доставка, но при чтении диффа или теста я вижу выбранный режим.
Где я оставляю bool и Option
bool подходит для вопроса с однозначным ответом «да» или «нет». Например, имя is_empty() уже объясняет, что означает результат. Дополнительный тип здесь ничего не добавляет.
В языках с именованными аргументами они тоже могут прояснить вызов. Иногда достаточно отдельного метода с понятным именем; такой вариант для флагов предлагает Мартин Фаулер. Если режим нужно хранить и передавать между функциями, лучше предпочитать отдельный тип.
Option оставляю, когда значение может отсутствовать и смысл отсутствия однозначен для вызывающего кода. В результате поиска по ключу None означает «ключ не найден». В обобщённом коде, которому важно только наличие значения, Option тоже достаточно.
В сгенерированном коде я постоянно встречаю true, false и None, которые приходится расшифровывать. Я всё ещё читаю код своих и чужих болванчиков и хочу, чтобы выбранные режимы и состояния были названы явно.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.