Синтаксические размышления о типах-представлениях (View Types)

— 2025-04-04 Yoshua Wuyts Источник

Вот одно глупенькое маленькое прозрение, которое у меня было на днях: если прищуриться, то и Типы-представления (View Types), и Типы-паттерны (Pattern Types) выглядят как облегчённые формы Уточнённых типов (Refinement Types)1. Оба позволят ограничивать типы, но слегка разными и дополняющими друг друга способами. Давайте взглянем на это на примере структуры Rgb, содержащей поля для отдельных каналов Красного, Зелёного и Синего, хранящихся как usize2:

#![allow(unused)]
fn main() {
#[derive(Debug, Default)]
struct Rgb {
    r: usize,
    g: usize,
    b: usize,
}
}

Типы-паттерны дадут нам возможность напрямую использовать такие вещи, как диапазоны или элементы перечислений, в сигнатурах типов. В следующем примере тип usize уточняется так, чтобы статически разрешать только значения от 0 до 255. Это похоже на типы NonZero* в стандартной библиотеке, но как часть языка и применимо с произвольными паттернами:

#![allow(unused)]
fn main() {
impl Rgb {
    fn set_red(&mut self, num: usize is ..255) { .. }
    //                                   ^^^^^^^^ паттерн диапазона
}
}

Типы-представления касаются сегментирования полей, содержащихся в self, чтобы несколько (изменяемых) методов могли работать с одним и тем же типом, не вызывая проблем с проверкой заимствований. В следующем примере мы предоставляем изменяемый доступ к отдельным полям с помощью отдельных методов. Ни один из этих методов не захватывает пересекающиеся заимствования полей в Self. Это означает, что мы можем свободно вызывать все эти методы, наблюдать за их возвращаемыми типами, и у нас не будет ошибок от borrow checker. Вот пример, использующий синтаксис из последнего поста Нико:

#![allow(unused)]
fn main() {
impl Rgb {
    fn red_mut(self: &mut { r } Self) -> .. { .. }
    //                    ^^^^^ представление
    fn green_mut(self: &mut { g } Self) -> .. { .. }
    //                      ^^^^^ представление
    fn blue_mut(self: &mut { b } Self) -> .. { .. }
    //                     ^^^^^ представление
}
}

Вот забавный вопрос: что произойдёт, если мы объединим Типы-паттерны и Типы-представления? Оба служат разным целям, и я знаю, что у меня есть случаи, где я хотел бы их совместить. Так как бы это выглядело? В абстрактном виде кажется, что мы получим что-то вроде следующего:

#![allow(unused)]
fn main() {
impl Rgb {
    fn foo(self: &mut { r, g } Self is Self { r: ..255, g: ..255, .. }) {}
    //                ^^^^^^^^      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //              представление               паттерн
}
}

Мне это кажется слишком много для чтения. Но также довольно... избыточно? И Типы-представления, и Типы-паттерны здесь уточняют Self. Я бы ожидал, что мы сможем их объединить. А что, если бы Типы-представления и Типы-паттерны вместо этого занимали одну и ту же синтаксическую позицию. Есть причина, по которой Типам-представлениям приходится использовать is, так давайте использовать его. С этим мы могли бы переписать наш предыдущий пример вот так:

#![allow(unused)]
fn main() {
impl Rgb {
    fn foo(&mut self is Self { r: ..255, g: ..255, .. }) {}
    //               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                   представление + паттерн
}
}

Это кажется мне намного более читаемым. Объединение обеих возможностей здесь кажется гораздо менее хлопотным и, возможно, даже... приятным? Это обновлённое обозначение также повлияло бы на наш предыдущий пример с Типами-представлениями. Используя обновлённую нотацию, это теперь записывалось бы так же, но без использования паттернов:

#![allow(unused)]
fn main() {
impl Rgb {
    fn red_mut(&mut self is Self { r, .. }) -> .. { .. }
    //                           ^^^^^^^^^ представление
    fn green_mut(&mut self is Self { g, .. }) -> .. { .. }
    //                             ^^^^^^^^^ представление
    fn blue_mut(&mut self is Self { b, .. }) -> .. { .. }
    //                            ^^^^^^^^^ представление
}
}

Мне кажется, что это довольно аккуратно унифицировало бы то, как мы говорим об уточнениях в сигнатурах функций. В плане нотации это переосмысливает Типы-представления как Типы-паттерны с игнорируемыми полями. Хотя нам не обязательно принимать эту нотацию: больше всего меня волнует, чтобы мы подумали о том, как обе функции предназначены для совместной работы в языке, чтобы создать целостный опыт. Спасибо за чтение!

Примечания


  1. Мне нравится термин «лёгкие уточнённые типы» для категории расширений, включающих типы-паттерны и типы-представления. Для меня это напоминает лёгкие формальные методы: менее тщательные, чем полный вариант, но с невероятной отдачей при относительных затраченных усилиях. ←

  2. Не слишком задумывайтесь, почему мы храним эти значения как usize. Это немного глупо. Для целей этого примера просто представьте, что есть какая-то причина, по которой мы должны делать именно так. ←