?

Log in

о читабельности кода - Оживший юзерпик

May. 30th, 2013

11:30 am - о читабельности кода

Previous Entry Share Next Entry

ППКС. Оригинал взят у avva в о читабельности кода

Я беседовал сегодня с Г. о разных способах выразить то же самое на C/C++ и смежных языках, и действительно ли это влияет на быстроту и качество чтения и понимания кода. Вообще говоря в последние годы я обнаруживаю в себе все больше и больше неприязни к "хитрым" способам что-то выразить в коде, более лаконичным, чем простой и наивный способ сказать то же самое, даже если он занимает больше строк кода. Почти все "хитрые" трюки привлекают к себе внимание и задерживают это внимания на себе, снижают скорость чтения кода и ясность его понимания. Но ровно то же самое я мог бы сказать и пять лет назад, а вместе с тем мое отношение изменилось еще дальше в сторону против трюков; наверное, мне нравится в коде еще большая ясность и прозрачность, чем раньше, и "трюками" я считаю вещи, которые другие программисты, наверное, сочтут совсем обыденными. Не захожу ли я в этом слишком далеко?

Вот два примера - мелких, и, может, даже мелочных, но иллюстрируют тему.

Пару лет назад мы говорили с Г. ровно о том же и тогда я сказал ему: в юности меня раздражала идиома работы с указателем if (p != NULL), и вместо нее в своем личном коде я всегда писал if (p) (и еще указателям присваивал 0, а не NULL). А сейчас, сказал я, предпочту первый вариант. Попытался объяснить, почему: когда читаешь такой код, как if(p), то пусть на долю секунды, но твое внимание отвлекается на него и ты делаешь приведение типов "в уме". Я могу сколь угодно твердо знать и помнить, что именно означает указатель в булевом контексте, но когда я читаю строки кода до того, я все равно помню, что вот это - указатель, а вот это - булево (логическое) выражение, и они совершенно разные вещи. Оператор != дает мне перейти от одного к другому, совершенно не задумываясь об этом переходе, именно потому, что он есть, этот оператор, он явно говорит: сейчас я вам дам булевое значение. А в случае if(p) мне нужно этот переход сделать самому, и это хоть на крохотную долю секунды, но отвлекает меня от действительно важных вещей.

Сегодня мы вспомнили ту старую беседу, потому что обсуждали похожий пример, в котором моя точка зрения показалась Г. совсем уж возмутительной (в случае с if(p) он говорит, не соглашается с моим выбором, но понимает логику). У меня есть в конце работы функции переменная, скажем, results. Функция возвращает булевое значение. Как мне написать выход:


1. return results > 0;


или


2. if (results > 0) {
  return true;
} else {
  return false;
}


(не обращайте внимания на скобки во втором варианте, это эстетика, если они вам не нравятся, представьте его без них). Я сказал, что хоть самому это странно говорить, пожалуй, предпочту второй вариант. Г. вначале подумал, что я пошутил так. А я не шучу. Логика та же самая. У меня нет никаких проблем понять первую форму, более того, мне естественно именно первую форму написать и это мое первое побуждение, но когда я думаю о том, как через полгода буду этот код читать (или кто-то другой будет), то, поколебавшись, выбираю вторую форму. Это не очевидный для меня выбор, понятно, что жалко тратить несколько строк, там где одной, вполне очевидной, хватает, но все-таки ясность чтения пересиливает.

У меня есть два аргумента против первой формы. Во-первых, почему это "трюк", почему привлекает внимание? Потому что мы относимся (кстати, прошу помнить, что я говорю о себе, все эти "мы" условные и гипотетиечские, если у вас это устроено по-другому, я не против), так вот, мы относимся к булевым значениям иначе, чем к целым числам, строкам, числам плавающей точкой. Все эти типы для нас - единицы информации, а булевы значения, как бы это сказать, единицы решения (information units и decision units). Мы привыкли в коде видеть булевы значения в одном из трех контекстов: 1) литералы true/false в аргументах и возвратных значениях функций; 2) внутри контрольных структур: if while for итд.; 3) аргументы логических операторов == != < итд. Причем третий контекст обычно содержится внутри второго, контекста "решения, что делать", а первый тоже можно считать его под-видом, только отдаленным во времени, но особенно прозрачным образом (скажем, если мы вернули true, то ожидаем, что кто-то тут же с этим значением сделает что-то "решительное", связанное с решением). Все другие использования булевых значений оказываются "странными" и привлекают наше внимание. Я не хочу эту "странность" преувеличивать, она, может, минимальна, и конечно при чтении все очевидно, но все же она есть. Иногда эта странность оправдана, потому что ее требует логика задачи. Скажем, при вычислении сложных и запутанных булевых значений стоит положить промежуточный результат в переменную. Или хранить в булевой переменной done условие окончания цикла. Или держать вектор булевых значений для чего-то. Во всех этих случаях, конечно, надо использовать булевые значение в этих немного более редких контекстах (хотя насчет "done" я не уверен - он настолько обычен, что редким трудно счесть, может, его стоит записать в собратья первого пункта выше), что поделать, это нужно "для работы". А в "return results > 0" это использование булевого значения скорее несколько фривольное, чем "для работы", поэтому крохотная задержка внимания, которого оно требует, неоправдано.

О втором аргументе я подумал чуть позже, и вот он какой. Я пытался наблюдать за собой, как именно я воспринимаю "return results > 0". И вдруг понял, как это охарактеризовать: зная, что функция возвращает булевое значение, я неизбежно, читая эту строку, делаю в уме такой небольшой танец на месте. Я думаю (пусть не словами, а сильно сокращенными ощущениями, но все же): "если больше нуля, то true, если нет, то false". Если так, то так, а если нет, то этак. Шаг влево - так, шаг вправо - этак. Выходит, что я в уме продумываю ровно то же, что и при чтении более длинной формы с if (results > 0). Но если она все равно у меня в уме возникает, так пусть и на экране будет. Несовпадение знаков на экране и продумывания в уме и вызывает это крохотную задержку при чтении, и ситуация тут аналогична if(p). А вот, скажем, если бы было написано что-то вроде "return IsPositive(results);", то это у меня не вызывает танца в уме, тут нет никакой задержки. Танец как бы откладывается до того момента, когда я прочитаю код IsPositive. (Это не значит, конечно, что надо именно так писать - у дополнительной функции и дополнительного уровня косвенности есть своя когнитивная цена. Я лично так писать не стану.)

Вот так как-то.

Comments:

[User Picture]
From:drf_ckoff
Date:May 30th, 2013 07:43 am (UTC)
(Link)
второй вариант тоже плохой. правильно:
 if (results > 0) {
  return true;
}

return false;
(Reply) (Thread)
[User Picture]
From:kiltum
Date:May 30th, 2013 09:32 am (UTC)
(Link)
ВО! Поддерживаю всеми руками и ногами
(Reply) (Parent) (Thread)
[User Picture]
From:drf_ckoff
Date:May 30th, 2013 07:44 am (UTC)
(Link)
хотя после этих ваших функциональных языков сильно тянет писать именно "return resuls > 0;"
(Reply) (Thread)
[User Picture]
From:svv
Date:May 30th, 2013 09:43 am (UTC)
(Link)
Это была моя первая мысль, когда я прочитал этот пост у Аввы -- нелюбовь писать "return results > 0;" (что мне кажется наиболее естественным, понятным и читаемым вариантом) наверняка как-то связана со слишком длительной работой с императивным кодом.
(Reply) (Parent) (Thread)
[User Picture]
From:votez
Date:May 30th, 2013 10:22 pm (UTC)
(Link)
поддерживаю. Хотя меня тянет писать просто "results > 0" . Маленький, понятный кусок.
(Reply) (Parent) (Thread)
From:dmzlj
Date:May 31st, 2013 03:40 am (UTC)
(Link)
этот вариант плохо читается, потому что в нём не хватает скобок.
return (result > 0); уже нормально, а вариант в if-then-else уже похож на какой-то индусский код --- приводились какие-то такие похожие одизные примеры.
(Reply) (Parent) (Thread)
[User Picture]
From:dzz
Date:May 30th, 2013 07:44 am (UTC)
(Link)
Я уже давно считаю, что трюки - отдельно, а промышленный/полезный код - отдельно ;)
(Reply) (Thread)
[User Picture]
From:poige
Date:May 31st, 2013 04:16 pm (UTC)
(Link)
Только дятел будет считать «return results > 0;» трюком. Впрочем, это объясняет почему столько «промышленного» говнокода вокруг.
(Reply) (Parent) (Thread) (Expand)
[User Picture]
From:elzhov
Date:May 30th, 2013 07:50 am (UTC)
(Link)
Насчет "if (p)" думаю точно так же, и еще тут помимо приведения типа еще есть спрятанное сравнение, которое все равно вычисляется, и мне как-то легче на душе, когда я пишу if (p != NULL) :) И еще "!" в обратном условии вроде "if (!someVariable)" мне просто плохо видно, со скобкой сливается, тут однозначно if (someVariable == NULL)

А вот "return (results > 0)" лично у меня читается именно как IsPositive(results), никаких развертываний в 4 строки в уме не происходит.
(Reply) (Thread)
[User Picture]
From:wildsun
Date:May 30th, 2013 08:21 am (UTC)
(Link)
забавно: вчера вечером вставлял мозги младшему на ту же тему (у него ЕГЭ по информатике сегодня).
(Reply) (Thread)
[User Picture]
From:antony_mk
Date:May 30th, 2013 08:32 am (UTC)
(Link)
абсолютно верно. Но до этого надо или достарперить, или в молодости поработать в конторе, где стиль кода жестко унифицирован.
Помнится, в IBS нас очень жестко дрючили за трюки, регламентировано было все, вплоть до правил именования таблиц, переменных и комментирования.
Мы по молодости матерились, но можно было +- спокойно прочитать код человека, уволившегося год назад.
(Reply) (Thread)
[User Picture]
From:besm6
Date:May 30th, 2013 08:58 am (UTC)
(Link)
Вы с ним ненавязчиво "забываете", что более длинный код как минимум дольше читается. Больше времени уходит на парсинг. И что на то, чтобы вспомнить, чему соответствует p, требуется время, как минимум сравнимое с "мысленным приведением".

Ну, то есть на мой взгляд if (found_ptr) понимается быстрее, чем if (p != NULL) и не медленнее, чем if (found_ptr != NULL). Потому как вообще-то на самом деле там if (found).

И уж точно return (num_of_matches > 0) понимается в разы быстрее, чем if (results > 0) return true else return false... А вот скобки там как раз не лишние для понимания.

Ну и классика жанра: если тело функции целиком не влезает в десяток строк, ее невозможно быстро понять. За счет разницы между "охватывается взглядом" и "не охватывается взглядом". Поэтому вариант

if (results > 0) {
return true;
} else {
return false;
}

занимающий половину этого объема, делает небыстропонимаемой абсолютно любую функцию.
(Reply) (Thread)
[User Picture]
From:jsn
Date:May 30th, 2013 11:06 am (UTC)
(Link)
Очень согласен.

Я бы ещё добавил стандартное "where's fun in that?". Умный программист, возможно, будет делать больше ошибок (как при чтении/отладке, так и при написании) от скуки и монотонности излишне вербозного и прямолинейного кода, чем от трюков и краткости. Кроме того, эрудированный программист на месте краткой формы / трюка видит вещь знакомую, простую и понятную, часто для этого не нужно ни парсить, ни разбирать логику конструкции. А про менее умных / эрудированных у вас в следующем комменте вполне раскрыто, по-моему.
(Reply) (Parent) (Thread) (Expand)
(no subject) - (Anonymous) (Expand)
[User Picture]
From:votez
Date:May 30th, 2013 10:29 pm (UTC)
(Link)
люто плюсую
(Reply) (Parent) (Thread)
[User Picture]
From:besm6
Date:May 30th, 2013 09:05 am (UTC)
(Link)
Вот point-free style в хаскеле, если он бурно содержит flip и curry/uncurry - да, усложняет чтение на порядок за счет сокращения записи втрое. А если без фанатизма, то не усложняет :)

Еще, конечно, важно, на кого ты рассчитываешь читабельность - на студента, на старпера или на программиста. В паскале или жабе разницы, как я понимаю, нет, в C/C++ уже есть, а если дело доходит хотя бы до элементов функциональщины, результаты одного действия в зависимости от целевой аудитории могут быть прямо противоположными.

И если тебе нужна надежная программа, то на студента и на старпера рассчитывать не надо - их нельзя подпускать к этому коду. Ну, то есть способного студента под присмотром можно, но ему как раз для развития нужно, чтобы код был рассчитан на программиста.
(Reply) (Thread)
[User Picture]
From:votez
Date:May 30th, 2013 10:27 pm (UTC)
(Link)
вы не поверите, какую заковыристую херню иногда можно встретить в жава или шарпокоде какого-нить недавнего студента... Хотя С/С++ - отличный способ выстрелить соседу в ногу через свою голову, но глупость и желание выпендриться выходят за эти рамки везде...
(Reply) (Parent) (Thread) (Expand)
[User Picture]
From:kiltum
Date:May 30th, 2013 09:30 am (UTC)
(Link)
За return results > 0; лично я убиваю до кого дотягиваюсь.

1. Нечитабельно.
2. В случае смены типа переменной results или типа возврата функции - полня херня будет (хотя воде компилятор должен сругаться, но хз, вдруг там выше прагма какая-нибудь)
3. комментарии писать негде
(Reply) (Thread)
[User Picture]
From:poige
Date:May 30th, 2013 06:02 pm (UTC)
(Link)
Ну так пишите на Паскале в пединституте, а Си не трогайте — Керниган и Ритчи его не для вас создавали.

Edited at 2013-05-30 06:06 pm (UTC)
(Reply) (Parent) (Thread) (Expand)
[User Picture]
From:rblaze
Date:May 30th, 2013 10:04 am (UTC)
(Link)
return (results > 0);

Всего одна пара скобок сразу настраивает мозг на правильный контекст и не заставляет бегать глазами по строчкам.
(Reply) (Thread)
[User Picture]
From:crimcat
Date:May 30th, 2013 11:49 am (UTC)
(Link)
Читал уже. Для меня выглядит странно. Ибо умение пользоваться инструментарием языка программирования с читаемостью кода мало связано.
И ладно, это хоть пока кусочек из C. А что делать с имплиситами в Скале? Забанить навсегда?
PS. И да, в целом синтаксис return expr немножко нелогичен, но можно договориться с самим собой ставить скобочки для общности;)
(Reply) (Thread)
[User Picture]
From:votez
Date:May 30th, 2013 10:36 pm (UTC)
(Link)
к имплиситам в скале подпускать токо после экзамена на соответствие и крупного денежного залога. К макросам - после защиты диссертации и получения контактов всех кровных родственников. Я даже серьёзно.
А то мощнейший движок выведения типов дополняется маленькой, безусловно мегаполезной штуковинкой, способной отключать тормоза в произвольный момент.
По поводу читаемости и инструментария - абсолютно согласен. Читаемость приходит после прочтения Фаулера и лет саппорта своего и чужого кода, а инструментарий - после StackOverflow и лет разработки по типу "сдал-забыл".
(Reply) (Parent) (Thread)
[User Picture]
From:leonov
Date:May 30th, 2013 01:39 pm (UTC)
(Link)
Отметился там, уточню тут. Идея "долой трюки" совершенно правильная, пример негодный. Во втором примере страдает именно читабельность - чтобы понять, что происходит, нужно прочитать на три строчки кода больше, ведь нет никакой гарантии, что в этих ветках не случилось что-то еще, от чего может зависеть результат.

Фактически вместо одного возвращаемого булевского значения приходится иметь дело с тремя - результат операции, true в одной ветке, false в другой, причем нужно убедиться, что два последних эквивалентны результату операции. И это спотыкание гораздо дороже, чем описанный танец.

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

И мне кажется, что описанные проблемы с "не таким" восприятием bool зависят от того, был ли этот самый bool в языке, на котором набивались основные шишки при обучении программированию.
(Reply) (Thread)
[User Picture]
From:maxcom
Date:May 31st, 2013 08:39 am (UTC)
(Link)
помоему если у автора проблемы с пониманием "return result > 0", то пора ему уже заканчивать заниматься программированием.
(Reply) (Thread)
[User Picture]
From:tobotras
Date:May 31st, 2013 09:48 am (UTC)
(Link)
Не читайте между строк, там всё наврано!

У автора нет проблемы с пониманием «return result > 0». У автора есть наблюдение, что понимание «return result > 0» требует больших ментальных усилий, чем развёрнутая форма.
(Reply) (Parent) (Thread) (Expand)
[User Picture]
From:zaitcev
Date:June 1st, 2013 01:52 am (UTC)
(Link)
Я наоборот мигрировал, причем отбивался еще. Но Гарзик меня заставил перейти на if (p). А потом привык. Что касается return, то у мужика явно уже соображалка сдает. Ему следовало посложнее пример выбрать, там было бы ясно, что он хочет сказать. Развернуть (фоунд > 0) может только конченный, на что в комментариях уже указали, с характерной русской свирепостью.

Лучше бы порассуждал про длину строки и переносы, вот была бы потеха.
(Reply) (Thread)