Rust-разработчик применил принцип «Parse, don't validate» на практике
Rusty thoughts on "Parse, don't validate"
Автор разбирает, как принцип «Parse, don't validate» из статьи Алексис Кинг работает в Rust. На примере NonEmpty, NonZero и AbsPathBuf из rust-analyzer он показывает, как типы помогают избавиться от повторных проверок и сделать невозможные состояния невыразимыми. Отдельное внимание уделено постепенному уточнению типов и десериализации JSON через serde.
Основная проблема в том, что Vec — это фундаментально тип, который может быть пустым; мы можем таскать за собой комментарий «этот не может быть пустым, честное слово!» по всему коду, но формально это ничем не проверяется.
- articulatepang
Я предпочитаю чуть более общее правило: Make Illegal States Unrepresentable. Правило «Parse, don't validate» — это частный случай MISU.
В чём разница? MISU применимо даже тогда, когда никакого преобразования, похожего на парсинг, не происходит. Например, если у вас есть переменная, представляющая текущее состояние сетевого соединения, и, скажем, оно может быть Disconnected или Connected к какому-то IP-адресу (это сильное упрощение).
Тогда одним из способов было бы
struct {
connected: bool,
peer_ip: int32
}
Проблема в том, что это позволяет представить нелегальное/бессмысленное состояние: мы отключены, но там всё ещё висит какой-то мусорный старый peer_ip. Хуже того, мы могли бы написать
struct {
connected: bool,
peer_ip: Option<int32>
}
Теперь у нас может быть connected = true, но peer_ip = None.
Решение — использовать сумму типов:
type connection =
Disconnected
| Connected of int32
(извините за выдуманный синтаксис; надеюсь, это понятно всем, кто знаком с Rust.)
«Make Illegal States Unrepresentable» применимо по всей вашей программе, на каждом интерфейсе между модулями или функциями программы, включая, помимо прочего, парсинг входных данных.
- Fluorescence
Не уверен, что этот тип — хороший совет:
pub struct NonEmpty<T> {
pub head: T,
pub tail: Vec<T>,
}
Вам придётся вручную реализовывать трейты, чтобы поддержать эргономику срезов и итерации, а также дорогостоящее перевыделение памяти, если нужно передать владение как Vec:
Я бы ожидал:
pub struct NonEmpty<T> {
v: Vec<T>,
}
Конструктор обеспечивал бы инвариант, а затем вы бы реализовали Deref и DerefMut для [T], чтобы получить обычные len/is_empty/индексацию/итерацию, передачу как &[T] в другие функции и мутацию значений (что не может нарушить инвариант).
Чтобы менять длину, сохраняя инвариант, — на ваш выбор, например:
- добавить .into_vec() для unwrap/mutate/rewrap
- добавить сохраняющие инвариант мутаторы на ваш выбор
- OptionOfT
По поводу сноски:
> [1] Другие языки — такие как Go или Python — имеют проверку во время выполнения, которая вызывает какое-то исключение или панику при обращении к lst[0] для пустого списка или среза.
В Rust есть то же самое. Обращение к `Vec` по индексу идёт через трейт index: https://doc.rust-lang.org/std/ops/trait.Index.html#tymethod....
Vec реализует Index здесь: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...
Index у Vec делегирует срезу: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...
Срез делегирует... интринсикам: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...
Которые вставляют проверку границ: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...
Проверка границ: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...
Так что я бы сказал, что сноска неверна.
- jelder
Это отлично. Алексис Кинг на самом деле говорила, что если бы она знала, насколько популярной станет «Parse, Don’t Validate», она написала бы её на языке, более широко используемом, чем Haskell.
- jph
Хорошая статья о сильных сторонах типов в Rust. Если вам это нравится, возможно, вам будет интересно, как построить собственные возможности парсинга. Мне нравятся Rust-крейты Winnow и Nom, а также Rust-трейты From и Into.