?

Log in

No account? Create an account
о читабельности кода - По делам сюда приплыл, а не за этим [entries|archive|friends|userinfo]
Anatoly Vorobey

[ website | Website ]
[ userinfo | livejournal userinfo ]
[ archive | journal archive ]

Links
[Links:| English-language weblog ]

о читабельности кода [май. 29, 2013|10:19 pm]
Anatoly Vorobey
[Tags|]

Я беседовал сегодня с Г. о разных способах выразить то же самое на 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:
Страница 1 из 5
<<[1] [2] [3] [4] [5] >>
[User Picture]From: amigofriend
2013-05-29 07:25 pm
Ну уж совсем-то хардкорить не обязательно.

if (results > 0)
return true;

return false;
(Ответить) (Thread)
[User Picture]From: avva
2013-05-29 07:27 pm
Да, можно и так. Это все с моей точки зрения несущественные различия в сравнении с return results > 0;

(Я предпочту поставить else, но это по-моему чистая эстетика).

(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: orleanz
2013-05-29 07:31 pm
а как Вам джавная избыточность?

(имею в виду знаменитую просьбу джависта в столовой: " Паблик статик файнал Борщ борщ нью Борщ, пожалуйста")
(Ответить) (Thread)
[User Picture]From: cema
2013-05-29 09:42 pm
Should not be static, he wants his own portion.
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: ahaxopet
2013-05-29 07:34 pm
Мне кажется, это еще от языка зависит. Я пока не совсем понимаю почему, но в C я скорее напишу "if( p != NULL)", а в питоне вполне обойдусь простым "if p:". Аргумент про танец в уме я понимаю, но цена этого танца в разных языках, как ни странно, разная.
(Ответить) (Thread)
[User Picture]From: avnik
2013-05-30 01:43 am
Это пока не вляпаемся в разницу между if not p: и if p is None:
(0 is not None, и тем более имя в памяти __zero__ и __nonzero__
(Ответить) (Parent) (Thread)
[User Picture]From: plakhov
2013-05-29 07:35 pm
К чертям ментальное. Что насчет высоты экрана и времени жизни колёсика мышки?
(Ответить) (Thread)
[User Picture]From: romikchef
2013-05-29 08:37 pm
В теории мы сюда должны попадать не колесом, а ctrl-кликом...
(Ответить) (Parent) (Thread)
[User Picture]From: bvlb
2013-05-29 07:37 pm
как насчет:

return bool(results > 0);

?

Edited at 2013-05-29 19:37 (UTC)
(Ответить) (Thread)
From: lazyreader
2013-05-29 08:21 pm
неплохо, кстати.
(Ответить) (Parent) (Thread)
[User Picture]From: alexeybobkov
2013-05-29 07:51 pm

Не вполне серьёзно :)

Если я увижу код типа 2, я обязательно потеряю время на секундное раздумье: "А почему так многословно написано? Нет ли тут какого-нибудь подвоха? Перечитаю-ка я эти строчки ещё разок, может, я чего-то не заметил!"
А когда после перечтения обнаружится, что подвоха нет, и код 2 делает то же, что делал бы код 1, я ещё потрачу долю секунды на раздражение :)
(Ответить) (Thread)
Вот-вот. Есть сложившиеся идиомы, и их желательно уметь опознавать сразу.

Читал я как-то статейку на тему "почему нельзя преподавать Си первокурсникам", там автор плакался из-за конструкций типа if(fork()) - потому что студенты их не понимают, не переписав в виде int pid = fork(); if(pid != 0) - но при этом для человека, регулярно использующего fork() они выглядят совершенно естественно. Вот эту дистанцию между студентом, тщательно раскрывающим идиоматическую конструкцию и "профессионалом" мы и называем "опытом".
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: dimanium
2013-05-29 07:55 pm
Удивлён, но это, конечно, дело вкуса.

Мне кажется, что вариант "return (results > 0)" воспринимается более естественно, если говорить не о значениях и контрольных структурах языка, а о вычислимости выражений. Другими словами, если у меня есть логическое выражение "(results > 0)", то для меня более естественно сразу вернуть результат его вычисления. А в конструкции с if-then-else я сначала смотрю на этот результат, и если он равен "true", то явно возвращаю "true", и если "false", то явно возвращаю "false" -- то есть я усложнил выражение, не добавив ничего нового.

Кстати, в таком случае было бы очень интересно узнать ваше отношение к логическим операциям в целом. Условно говоря, вы предпочитаете "(a && b)" или "if a then b else false"? :)

Edited at 2013-05-29 19:55 (UTC)
(Ответить) (Thread)
From: vasja_iz_aa
2013-05-29 07:59 pm
я напишу коротко если этот ноль означает только что совершенную неудачную попытку что то сделать, в случаях похожих на
p= calloc (n,sizeof(int));
if (p) ......

если же он откуда то там издалека пришел, то чаще напишу if(p!=NULL)
(Ответить) (Thread)
[User Picture]From: leonov
2013-05-29 08:00 pm
Ясность чтения во втором случае только страдает - окончательное понимание того, что происходит в этом коде, появляется только после чтения всех четырех строчек. Ведь там может быть что угодно, приводящее к отличному результату от однострочного варианта. И задержка эта гораздо больше, чем уходит на описанный мысленный танец.
(Ответить) (Thread)
[User Picture]From: kaathewise
2013-05-29 08:04 pm
Поддерживаю.
(Ответить) (Parent) (Thread)
[User Picture]From: burrru
2013-05-29 08:02 pm
Мудрый подход.
(Ответить) (Thread)
[User Picture]From: _winnie
2013-05-29 08:09 pm
Последовательные сокращения кода - начинают между собой взаимодействовать давая ещё большее сокращение. Как играх-"шариках", когда удаление одной кучки шариков вызывает падение верних в пустое место, и схлопывание дополнительных скоплений.

Например, была функция IsPositive в которой двухэтажная этажерка if. Заменяем этажерку на краткую запись return (result > 0). Оппа, теперь функцию можно IsPositive заинлайнить в место вызова! При чтении кода уже не нужно не то, что микроусилий по дешифровке, но и макроусилий по переходу на определение IsPositive, и слота в памяти на доп-сущность. В вызывающей функции была переменная result? Выкидываем, заменяем выражение result в выражении (result > 0) на инициализатор result, например my_container.count(input). Снова однострочная функция? Выкидываем, инлайним её в места вызова. Функций в классе больше нет, кончились? Выкидываем класс. Выкидываем файл с классом. И тд.

(Ответить) (Thread)
[User Picture]From: gdy
2013-05-30 08:58 am
+1
(Ответить) (Parent) (Thread)
[User Picture]From: beldmit
2013-05-29 08:09 pm
Вариант 2 куда удобнее, если надо добавить дополнительные действия (отладку, например).
(Ответить) (Thread)
From: 109518
2013-05-29 09:01 pm
А ведь да. И потом лучше переводится на человецеский язык:

"если х больше нуля вернуть Т иначе вернуть Ф" лучше чем "вернуть х больше нуля"
(Ответить) (Parent) (Thread)
[User Picture]From: cmm
2013-05-29 08:10 pm
во-первых, я бы вместо этого дикого if-then-else написал (будучи принужден style guidе'ом либо другим видом тирании) "(result > 0 ? true : false)".  но хрен бы с ним, неважно.

a важно что есть серьёзная разница в (моём) восприятии "if (p)" vs. "if (p != NULL)" и "result > 0" vs. if-then-else: в первом случае более длинный вариант не сильно длиннее, это по прежнему один expression и одна строка.  if-then-else, конечно, может кем-то проще читаться чем "result > 0", но неужели возникающее по прочтении раздражение (б3ь, неужели нельзя было написать просто "result > 0"???) не перебивает всю пользу от теоретически повышенной читабельности?

Edited at 2013-05-29 20:15 (UTC)
(Ответить) (Thread)
[User Picture]From: krace
2013-05-29 09:30 pm

б3ь — это п2ь! =)
(Ответить) (Parent) (Thread) (Развернуть)
[User Picture]From: pphantom
2013-05-29 08:18 pm
Это правильный подход, но у него есть один изъян. В общем-то на каждом языке принято писать определенным образом. и такие конструкции на C/C++ действительно будут восприниматься как потенциальный подвох. Так что, вообще говоря, надо доводить идею до логического конца и менять используемый язык.
(Ответить) (Thread)
[User Picture]From: yucca
2013-05-29 08:18 pm
Для меня, если писать return (results>0), скобки как-то фокусируют взгляд и ускоряют понимание.
Но вот что мне интересно: если написать длинную версию на интервью, интервьюеры не подумают, что я "ненастоящий" программист?
(Ответить) (Thread)
From: 75dc287ea30b451
2013-05-30 07:41 pm
"Но вот что мне интересно: если написать длинную версию на интервью, интервьюеры не подумают, что я "ненастоящий" программист?"

Нет. Интервьюеры подумают: "О! Наконец-то хоть кто-то понимает, как надо программировать, чтобы оно потом работало, а не чтобы выпендриться!"

А возможные исключения из этого правила Вам в качестве места работы удовольствия, скорее всего, все равно не доставят.
(Ответить) (Parent) (Thread)
Страница 1 из 5
<<[1] [2] [3] [4] [5] >>