Неявность
• оригинал: withoutboats • перевод: bmusin & ozkriff • размышления • поддержите на Patreon
Часто, когда я обсуждаю дизайн Rust на RFCs или на internals-форуме с другими пользователями, я замечаю характерные высказывания насчёт явности. Обычно что-то в духе:
Мне не нравится
<дизайн возможности X>
, потому что он менее явный. Всякая магия уместна в<другом языке Y>
, а Rust — это явный язык, так что следует использовать<дизайн возможности Z>
.
Подобные комментарии жутко меня раздражают, потому что дают очень мало полезной обратной связи. Они лишь утверждают, что «явное лучше неявного» (при этом предполагается, что это бесспорное утверждение), и что какой-то дизайн менее явный, чем альтернатива (хотя часто даже не приводится объяснений, почему именно критикуемый дизайн менее явный), из чего следует, что их подход предпочтительнее.
В своей опубликованной ранее в этом году заметке Аарон пытался докопаться до сути вопроса явности, обсуждая размер контекста (reasoning footprint). Он попытался разбить понятия «явность» и «неявность» на составные части, чтобы подготовить почву для суждения о явности дизайна той или иной возможности. Я же хочу изложить немного другой взгляд на проблему и попытаюсь очертить в общих словах, что мы подразумеваем под словом «явный».
Английский — довольно нечёткий язык, в котором прилагательные имеют множества контекстно-зависимых значений, например, как используется слово «нечёткий» (fuzzy) в предыдущем предложении. Слово «явный» тоже многозначно, так что я не могу утверждать наверняка, что кто-то неправильно использует это слово. Однако я предлагаю выражать свои мысли при обсуждении «явности» более чётко, чтобы все лучше понимали, о чем именно идёт речь.
Что я подразумеваю под словами: «Rust — явный язык»
Часто, будучи озадачен словами «явное лучше неявного», я хочу просто занять противоположную сторону в этом вопросе, утверждая, что явность плоха, а неявность, наоборот, хороша. Хотя я считаю, что Rust довольно явен, но, когда я использую слово «явный», я подразумеваю нечто более конкретное, чем обычно понимается под этим словом. Моё мнение: Rust явен, потому что вы можете многое понять о вашей программе, просто читая её исходный код.
Например, вот несколько определений структур на Rust:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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)]
,
который означал бы что экземпляры типа всегда выделятся в куче.
Это могло бы быть довольно удобной формой записи распространённого шаблона:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 все равно
не жёстко гарантирует вызов деструкторов.
1 2 3 4 5 6 7 8 9 | 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, которая в то же время не локальна, является разрешение методов. Посмотрите на код:
1 2 3 4 5 6 7 8 9 | 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) (и объясните, почему вы считаете, что она должна быть ручной!).
-
Если вы думаете, что некая информация должна быть видимой в определённом участке кода, возможно вам стоит говорить о локальности в данном контексте (Опять же объясните, почему по-вашему это важно!).
Каждый из этих терминов — явный, шумный, тяжеловесный, ручной, локальный — является уместным для употребления в некоторых случаях, и неуместным в других. Почти всегда при выборе подхода к реализации функциональности требуется идти на компромиссы. Одним из способов определиться с выбором может являться рассмотрение, как он повлияет на (объяснённый Аароном) размер контекста.
Так что я прошу вас в следующий раз, когда вы будете обсуждать явность какой-то функциональности языка, точнее укажите о каком именно виде явности вы беспокоитесь и чётко объясните, почему именно вы считаете ваше предложение более разумным.