Pull to refresh

Неявность

Reading time7 min
Views9.3K
Original author: withoutboats

Часто, когда я обсуждаю дизайн Rust на RFCs или на internals-форуме с другими пользователями, я замечаю характерные высказывания насчет явности. Обычно что-то в духе:


Мне не нравится <дизайн возможности X>, потому что он менее явный. Всякая магия уместна в <другом языке Y>, а Rust — это явный язык, так что следует использовать <дизайн возможности Z>.

Подобные комментарии жутко меня раздражают, потому что дают очень мало полезной обратной связи. Они лишь утверждают, что "явное лучше неявного" (при этом предполагается, что это бесспорное утверждение), и что какой-то дизайн менее явный, чем альтернатива (хотя часто даже не приводится объяснений, почему именно критикуемый дизайн менее явный), из чего следует, что их подход предпочтительнее.


В своей опубликованной ранее в этом году заметке Аарон пытался докопаться до сути вопроса явности, обсуждая размер контекста (reasoning footprint). Он попытался разбить понятия "явность" и "неявность" на составные части, чтобы подготовить почву для суждения о явности дизайна той или иной возможности. Я же хочу изложить немного другой взгляд на проблему и попытаюсь очертить в общих словах, что мы подразумеваем под словом "явный".


Английский — довольно нечеткий язык, в котором прилагательные имеют множества контекстно-зависимых значений, например, как используется слово "нечеткий" (fuzzy) в предыдущем предложении. Слово "явный" тоже многозначно, так что я не могу утверждать наверняка, что кто-то неправильно использует это слово. Однако я предлагаю выражать свои мысли при обсуждении "явности" более четко, чтобы все лучше понимали, о чем именно идет речь.


Что я подразумеваю под словами: "Rust — явный язык"


Часто, будучи озадачен словами "явное лучше неявного", я хочу просто занять противоположную сторону в этом вопросе, утверждая, что явность плоха, а неявность, наоборот, хороша. Хотя я считаю, что Rust довольно явен, но, когда я использую слово "явный", я подразумеваю нечто более конкретное, чем обычно понимается под этим словом. Моё мнение: Rust явен, потому что вы можете многое понять о вашей программе, просто читая ее исходный код.


Например, вот несколько определений структур на Rust:


struct Doggo {
    coat_color: Color,
    stamina: u32,
    love: u32,
    // NOTE: всегда true
    is_a_good_dog: bool,
}

struct Color(u8, u8, u8);

struct TennisBall;

struct Park {
   dogs: Vec<Doggo>,
}

struct Fetch<'a> {
    park: &'a Park,
    doggo: &'a Doggo,
    ball: TennisBall,
}

Я могу довольно много сказать о том, как эти структуры будут расположены в памяти, просто глядя на их определения:


  • Я знаю поля всех структур (в отличие от многих динамических языков).
  • Я знаю допустимые значения каждого поля (т.е. я знаю их типы).
  • Я знаю, что все данные (кроме вектора Doggos в Park) будут расположены на стеке.
  • Структура TennisBall не имеет полей, и оптимизатор просто выкинет ее при сборке.
  • Я знаю, что ссылки в Fetch будут указателями на Park и Doggo.
  • Принимая во внимание требования по выравниванию данных на моем процессоре, я могу довольно точно прикинуть размеры структур.

Примером неявности (в контексте приведенном выше), может служить точный порядок полей в этих структурах. Rust специально не определяет порядок полей в структуре, чтобы в зависимости от ситуации можно было оптимизировать её, переставив поля некоторым образом. Обычно вам и не нужно знать этот порядок, разве что при работе с unsafe-кодом.


Я бы сказал что подобная явность многих аспектов вашего кода обычно очень полезна и является сильной стороной Rust'а. Но надо помнить, что ради ее поддержания приходится идти на компромиссы: например, компилятор не может самовольно перенести данные из стека в кучу во время оптимизаций.


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


Другие значения слова "явный"


Явный — не значит шумный (verbose)


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


Одним из примеров является введение в язык оператора ?, который немного короче предыдущего макроопределения try!. Некоторые пользователи высказывали опасения, что из-за данного оператора будет проще проглядеть ранний выход из функции. В данном случае они хотели, чтобы синтаксис был более шумен, а не просто явен.


Я считаю, что все точки возврата из функции должны быть явными, но не обязательно шумными. Т.е., если я хочу выяснить, как функция возвращает значение, я должен иметь возможность это сделать, но это совсем не самое первое, на что я стану обращать внимание при чтении кода. Наоборот, особенно при пробрасывании ошибок по стеку через оператор ?, ранний выход вообще мало мне интересен при чтении кода.


Явный — не значит "обременительный"


Иногда пользователи говорят, что синтаксис некой ресурсоемкой операции должен быть тяжеловесным, чтобы отбить охоту лишний раз ей пользоваться. Например, пользователи могут считать достоинством языка меньшую элегантность создания объекта в куче по сравнению с созданием на стеке.


Часто в таких спорах используется слово "явность", хотя подобная "синтаксическая соль" совершенно ей ортогональна. На самом деле речь идет о большей "тяжеловесности" конструкций с целью показать нежелательность ее использования. Например, можно представить себе атрибут [repr(boxed)], который означал бы что экземпляры типа всегда выделятся в куче. Это могло бы быть довольно удобной формой записи распространенного шаблона:


struct Catters {
    inner: Box<CattersInner>,
}

struct CattersInner {
    color: Color,
    pounces: u32,
    naps: u32,
    meows: u32,
}

// С repr(boxed) это становится единой структурой:
#[repr(boxed)]
struct Catters {
    color: Color,
    pounces: u32,
    naps: u32,
    meows: u32,
}

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


Как и раньше, лично я не считаю, что написание кода для размещения переменных в куче должно быть обременительным. Мне не кажется, что размещение данных в куче должно быть поведением по умолчанию, но есть и не так мало ситуаций, когда размещение в куче предпочтительней размещения в стеке. Поэтому нам не следует раздражать пользователей или тем более давать им повод думать, что они что-то делают не так.


Явный — не значит ручной


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


Например, представим себе версию Rust, в которой drop надо вызывать вручную (заметим, что в текущем Rust вы не можете вызывать этот метод, однако для примера допустим, что он принимает self по значению). На самом деле это даже безопасно, потому что Rust все равно не жестко гарантирует вызов деструкторов.


fn string_processing(string: String, numbers: &mut Vec<u32>) {
    substrings = string.split_whitespace().filter(|s| s.starts_with('$'));
    for substring in substring {
        let n = substring.parse().unwrap();
        numbers.push(n);
    }
    // Нужно вызвать явно, иначе память строки "утечет"
    string.drop();
}

Если вы удалите строчку вызова drop, то выделенная для строки память утечет. Думаю что всем очевидно, что такой подход был бы хуже. Нет ничего плохого в автоматическом вызове деструкторов, потому что всегда можно понять, когда это произойдет, просто отследив окончание области видимости переменной.


Явный — не значит локальный


Еще в некоторых случаях под словом "явность" пользователи подразумевают явность внутри определённого участка кода. Это значит, что какая-то информация о коде должна быть понятна из изучения только определенной его части. Причем, она может быть любого размера — модуль, функция, выражение, и т. д. Если что-то явно на определенном участке исходного кода, то это не значит, что оно явно везде — слово "локальный" здесь гораздо уместнее.


Неявной возможностью Rust, которая в то же время не локальна, является разрешение методов. Посмотрите на код:


fn main() {
    let mut vec = vec![0, 1, 2];
    let x = vec.len();

    vec.extend([x, x + 1]);
    for elem in vec.into_iter() {
        println!("{}", elem)
    }
}

В данной функции мы вызываем три разных метода вектора — len, extend и into_iter. Каждый из которых принимает self по-своему (по ссылке, по изменяемой ссылке и по значению). Два метода определены для самой структуры Vec, а один — из типажа Extend. Ничто из этой информации не видно при взгляде только на приведенную функцию, однако все это становится "явным" при рассмотрении impl блоков у Vec<T>.


Напротив, оператор ? обладает такой локальностью. Можно представить, что ко всем функциям, возвращающим Result, которые вызываются из функции, которая тоже возвращает Result, автоматически применялся бы оператор ? (так работают исключения в подобных Java языках). Но мы решили, что не должно быть необходимости смотреть на интерфейс функции что бы понять, будет ли внутри нее работать неявный ранний выход. Думаю, что это хороший пример полезной локальности.


Заключение


В общем, если во время обсуждения вы собираетесь использовать слово "явный", то подумайте, не стоит ли вам более точно сказать, что вы имеете в виду:


  • Если вас заботит, является ли что-то достаточно очевидным, возможно вам стоит использовать слова "шумный" или "очевидный" (и обязательно пояснить почему вы считаете это важным!).
  • Если вы думаете, что стоит усложнить использование операции, возможно вам стоит называть это "обременительным" или "тяжеловесным" (и обязательно объясните, почему вы считаете что это действие не должно быть слишком удобным!).
  • Если вы считаете, что пользователи должны вызывать необходимую логику вручную (а не чтобы она случалась автоматически при определенных условиях или событиях), возможно вам стоит называть ее "ручной" или "явно вызываемой" (opt-in) (и объясните, почему вы считаете, что она должна быть ручной!).
  • Если вы думаете, что некая информация должна быть видимой в определенном участке кода, возможно вам стоит говорить о локальности в данном контексте (Опять же объясните, почему по-вашему это важно!).

Каждый из этих терминов — явный, шумный, тяжеловесный, ручной, локальный — является уместным для употребления в некоторых случаях, и неуместным в других. Почти всегда при выборе подхода к реализации функциональности требуется идти на компромиссы. Одним из способов определиться с выбором может являться рассмотрение, как он повлияет на (объясненный Аароном) размер контекста.


Так что я прошу вас в следующий раз, когда вы будуте обсуждать явность какой-то функциональности языка, точнее укажите о каком именно виде явности вы беспокоитесь и четко объясните, почему именно вы считаете ваше предложение более разумным.


Большое спасибо всем из сообщества rustycrate, кто участвовал в переводе, вычитке и радактировании данной статьи. А именно: bmusin, mkpankov, vitvakatu и sasha_gav.

Only registered users can participate in poll. Log in, please.
Как вы относитесь к «синтаксической соли» Rust'а?
25.71% У меня нет опыта с Rust, отчасти потому что эта соль и отпугивает27
8.57% Я много писал на Rust, но читать код все равно сложно9
54.29% Хороший баланс соли и сахара57
11.43% Я не буду возражать, если соли добавят еще больше!12
105 users voted. 61 users abstained.
Tags:
Hubs:
+33
Comments21

Articles