Часто, когда я обсуждаю дизайн 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.
Комментарии (21)
amarao
05.02.2018 13:46Когда говорят про неявность, подразумевают ровно одно: сайд-эффект грамматики не очевиден читающему, его трудно сформулировать кратким правилом, или он сильно зависит от рантайм-значений.
Пример плохой неявности — приведение типов при сравнении (==) в javascript.
Пример хорошей неявности — подстановка в метод класса self первым аргументом в python. (В нём есть пример хорошей явности — self явно объявляют в списке аргументов).ozkriff Автор
05.02.2018 14:02Хз, Я за последние пару лет видел порядочно обсуждений ЯП "на RFCs или на internals-форуме" (и еще много где), которые могли бы пройти намного более гладко, если бы участники сразу более точно указывали какой вышеуказнный сорт "явности" они имеют ввиду.
fcoder
05.02.2018 18:56Добавлю, что self первым аргументом в сигнатуре не просто так — он выражает тип передачи — заимствование, изменяемое заимствование или перемещение.
Ryppka
06.02.2018 15:41Все-таки мне кажется, что «многословность» или, например, «зашумленность», «захламленность», было бы лучше, чем «шумность». Без оригинального verbosity я бы затруднялся понять, что именно имеется в виду.
ozkriff Автор
06.02.2018 15:54Ага, мы над этим задумывались, потому оригинал на всякий и указан. Местами там используется noise — в итоге ни один из вариантов перевода на 100% нормальным не выглядит.
захламленность
Это точно плохой вариант из-за слишком негативной окраски :)
bmusin
По поводу опроса: не люблю соль, пусть лучше будет больше сахара,
а также операторов на всякие случаи жизни, например, <=>, как в Perl'е,
позволит писать более лаконичный код.
Возможно стоит добавить вариант — «соль не нравится/ее уже слишком много/не нужна».
Satim
Что то вроде такого?)
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++
.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.
------.--------.>+.>.
ozkriff Автор
Боюсь что это одна из фишек Ржавчины, язык с самого начала задумывался как знатно просоленный. Начиная с простейших вещей, вроде того что для создания изменяемой переменной надо писать не просто
let a = 0;
, аlet mut a = 0;
.Опять же, насколько я себе представляю, приоритетом является "написание удобного в поддержке кода", а не короткого. Есть мнение что эти цели часто друг другу противоречат.
За аккуратное и вдумчивое насыпание сахара отвечает https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html, по итогам порядочно RFC уже появилось и еще будет.
Sirikid
ozkriff Автор
А "соль" понятие размытое же. Возможен же язык, где не надо явно помечать изменяемую переменную? Вполне возможен, их полно. Раз от
mut
ов можно было бы избавиться при желании, значит это в каком-то смысле вполне себе "соль".Были же предложения ввести в язык
var
как сокращение дляlet mut
— их отвергли именно как противоречащие "соленому" духу языка, не поощряещему мутабельность.Antervis
в итоге печатать корректный с точки зрения мутабельности код проще, чем некорректный. Вон в си для иммутабельности нужен const, и в итоге win api много где const-некорректен. А ведь это должен быть пример хорошего кода.
В общем, я за оценку с точки зрения оценки именно результатов того или иного принятого решения
ozkriff Автор
Ну, эм, да. Соль и нужна для того что бы подталкивать людей писать хороший код. :)
Kobalt_x
а ничего что winnapi появилось до того как приняли ansi C и никаких const в языке ещё не было.
Antervis
а что, за почти 30 лет с момента появления ansi C нельзя было дописать const в несколько мест? С учетом того, что это никак не затронет ни легаси код, ни бинарную совместимость?
Ryppka
А ничего, что в C константность в объявлениях не работает? А что написано в определениях в WinAPI не видно?
mayorovp
Что вы понимаете под "константность в объявлениях не работает"?
Ryppka
В C cv-квалификаторы возвращаемых значений и параметров в объявлениях не имеют смысла, обычно вызывают предупреждения и, соответственно, редко используются.
В объявлении C-функции cv-квалификаторы аргументов осмыслены.
Но не возвращаемого значения.
Antervis
поясню зачем нужна const-корректность на простом примере:
Но проблема на самом деле даже не в том, что не хватает каста, а в том, что из сигнатуры функции непонятно, меняет ли она строку. А чтобы стало понятно, надо лезть в MDSN где как правило никаких подробностей ни на русском ни на английском.
Ryppka
// test08.c
int func_dumb(char *str);
int func_smart(char const *str);
void test() {
func_dumb("abc");
func_smart("abc");
}
Результат компиляции C:
XXXX$ clang -Wall -Wextra -c -std=c99 test08.c
Ничего, none, nada.
Результат компиляции C++: ворнинг.
XXXX$ clang++ -Wall -Wextra -c -std=c++11 test08.c
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
test08.c:6:13: warning: ISO C++11 does not allow conversion from string literal to 'char *' [-Wwritable-strings]
func_dumb("abc");
^
1 warning generated.