о читабельности кода
May. 30th, 2013 11:30 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
ППКС. Оригинал взят у
avva в о читабельности кода
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
Я беседовал сегодня с Г. о разных способах выразить то же самое на 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. (Это не значит, конечно, что надо именно так писать - у дополнительной функции и дополнительного уровня косвенности есть своя когнитивная цена. Я лично так писать не стану.)
Вот так как-то.
Вот два примера - мелких, и, может, даже мелочных, но иллюстрируют тему.
Пару лет назад мы говорили с Г. ровно о том же и тогда я сказал ему: в юности меня раздражала идиома работы с указателем 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. (Это не значит, конечно, что надо именно так писать - у дополнительной функции и дополнительного уровня косвенности есть своя когнитивная цена. Я лично так писать не стану.)
Вот так как-то.
no subject
Date: 2013-05-30 07:43 am (UTC)no subject
Date: 2013-05-30 09:32 am (UTC)no subject
Date: 2013-05-30 10:20 pm (UTC)(no subject)
From:no subject
Date: 2013-05-30 07:44 am (UTC)no subject
Date: 2013-05-30 09:43 am (UTC)no subject
Date: 2013-05-30 10:22 pm (UTC)no subject
Date: 2013-05-31 03:40 am (UTC)return (result > 0); уже нормально, а вариант в if-then-else уже похож на какой-то индусский код --- приводились какие-то такие похожие одизные примеры.
no subject
Date: 2013-05-30 07:44 am (UTC)no subject
Date: 2013-05-31 04:16 pm (UTC)(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2013-05-30 07:50 am (UTC)А вот "return (results > 0)" лично у меня читается именно как IsPositive(results), никаких развертываний в 4 строки в уме не происходит.
no subject
Date: 2013-05-30 08:21 am (UTC)no subject
Date: 2013-05-30 08:32 am (UTC)Помнится, в IBS нас очень жестко дрючили за трюки, регламентировано было все, вплоть до правил именования таблиц, переменных и комментирования.
Мы по молодости матерились, но можно было +- спокойно прочитать код человека, уволившегося год назад.
no subject
Date: 2013-05-30 08:58 am (UTC)Ну, то есть на мой взгляд 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;
}
занимающий половину этого объема, делает небыстропонимаемой абсолютно любую функцию.
no subject
Date: 2013-05-30 11:06 am (UTC)Я бы ещё добавил стандартное "where's fun in that?". Умный программист, возможно, будет делать больше ошибок (как при чтении/отладке, так и при написании) от скуки и монотонности излишне вербозного и прямолинейного кода, чем от трюков и краткости. Кроме того, эрудированный программист на месте краткой формы / трюка видит вещь знакомую, простую и понятную, часто для этого не нужно ни парсить, ни разбирать логику конструкции. А про менее умных / эрудированных у вас в следующем комменте вполне раскрыто, по-моему.
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From: (Anonymous) - Date: 2013-05-30 04:13 pm (UTC) - Expandno subject
Date: 2013-05-30 10:29 pm (UTC)no subject
Date: 2013-05-30 09:05 am (UTC)Еще, конечно, важно, на кого ты рассчитываешь читабельность - на студента, на старпера или на программиста. В паскале или жабе разницы, как я понимаю, нет, в C/C++ уже есть, а если дело доходит хотя бы до элементов функциональщины, результаты одного действия в зависимости от целевой аудитории могут быть прямо противоположными.
И если тебе нужна надежная программа, то на студента и на старпера рассчитывать не надо - их нельзя подпускать к этому коду. Ну, то есть способного студента под присмотром можно, но ему как раз для развития нужно, чтобы код был рассчитан на программиста.
no subject
Date: 2013-05-30 10:27 pm (UTC)(no subject)
From:no subject
Date: 2013-05-30 09:30 am (UTC)1. Нечитабельно.
2. В случае смены типа переменной results или типа возврата функции - полня херня будет (хотя воде компилятор должен сругаться, но хз, вдруг там выше прагма какая-нибудь)
3. комментарии писать негде
no subject
Date: 2013-05-30 06:02 pm (UTC)(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2013-05-31 03:45 am (UTC)2. А в случае смены требований это вообще будет неправильно.
3. Комментарии (к чему?) можно писать справа, после точки с запятой.
(no subject)
From:no subject
Date: 2013-05-30 10:04 am (UTC)Всего одна пара скобок сразу настраивает мозг на правильный контекст и не заставляет бегать глазами по строчкам.
no subject
Date: 2013-05-30 11:49 am (UTC)И ладно, это хоть пока кусочек из C. А что делать с имплиситами в Скале? Забанить навсегда?
PS. И да, в целом синтаксис return expr немножко нелогичен, но можно договориться с самим собой ставить скобочки для общности;)
no subject
Date: 2013-05-30 10:36 pm (UTC)А то мощнейший движок выведения типов дополняется маленькой, безусловно мегаполезной штуковинкой, способной отключать тормоза в произвольный момент.
По поводу читаемости и инструментария - абсолютно согласен. Читаемость приходит после прочтения Фаулера и лет саппорта своего и чужого кода, а инструментарий - после StackOverflow и лет разработки по типу "сдал-забыл".
no subject
Date: 2013-05-30 01:39 pm (UTC)Фактически вместо одного возвращаемого булевского значения приходится иметь дело с тремя - результат операции, true в одной ветке, false в другой, причем нужно убедиться, что два последних эквивалентны результату операции. И это спотыкание гораздо дороже, чем описанный танец.
Ну и скобки вокруг возвращаемого значения - да, заметно повышают комфортность чтения.
И мне кажется, что описанные проблемы с "не таким" восприятием bool зависят от того, был ли этот самый bool в языке, на котором набивались основные шишки при обучении программированию.
no subject
Date: 2013-05-31 08:39 am (UTC)no subject
Date: 2013-05-31 09:48 am (UTC)У автора нет проблемы с пониманием «return result > 0». У автора есть наблюдение, что понимание «return result > 0» требует больших ментальных усилий, чем развёрнутая форма.
(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:(no subject)
From:no subject
Date: 2013-06-01 01:52 am (UTC)Лучше бы порассуждал про длину строки и переносы, вот была бы потеха.