- Плохие, постоянно виснущие программы создаются программистами благодаря лени.
- Хорошие, быстро работающие программы создаются программистами благодаря лени.
— Почему так, и в чём разница?
— Всё просто. Разница в том, на что программисты распространяют лень.
У первых лень распространяется на самих себя — программистам лень делать лишние (по их мнению) действия. Как результат: плохие, постоянно виснущие программы. У вторых лень распространяется на их программу — она не делает лишних действий и, как результат, работает быстро, не зависает.
Вот и всё разница.
— Что имеется в виду под «ленивой программой», «ленивым кодом»?
— Имеется в виду код, следующий старым как мир принципам «Бритва Оккама», KISS и не выполняющий любых лишних действий для решения задачи, если задачу можно решить без их выполнения.
И действительно, любая задача требует для своего решения какой-то минимум действий, уменьшить количество которых или сократить время их выполнения невозможно. Невозможно посчитать MD5-хеш терабайтного файла быстрее, чем он прочитается с диска.
(И если вы не имеете представления о классах сложности и букве «О», советую вам его сформировать)
Проблема в том, что программа ленивых программистов совершит при решении задачи совершенно ненужные действия. Канонический, хоть и надуманный пример:
вычисление
- Ленивый программист беспокоится о количестве мыслей, которые его голове предстоит обдумать. Поэтому он не будет думать о количестве действий, которые его программе предстоит совершить, и будет решать задачу «в лоб»: напишет функцию, считающую факториал от аргумента, и функцию, считающую сумму элементов последовательности. В итоге (с учётом свёртки двух функций в одну) он получит следующее:
- Код: Выделить всё
For i = 1 To n
fac = 1
For j = 1 To i
fac = fac * j
Next j
sum = sum + fac
Next i
Ленивого программиста не волнует, что 20-ой итерации придётся заново перемножить 20 чисел, результат перемножения 19-и из которых уже был получен одну итерацию назад, и что он ровно в 20 раз меньше, чем результат перемножения текущих двадцати. Его не волнует, что вместо совершения бессмысленных умножений, уже ранее неоднократно проводившихся, достаточно умножить последнее произведение на 20. Его волнует другое: «Как бы сделать задание, затратив меньше своего времени и своих сил» (как будто они кроме него самого кого-то интересуют...). - Не ленивый программист не пожаелет времени (1,5 секунды), чтобы проанализировать процесс и понять, что в раз больше, чем .
В его коде не будет двойного цикла и лишних умножений:- Код: Выделить всё
sum = 1
fac = 1
For i = 2 To n
fac = fac * i
sum = sum + fac
Next i
- Думающий программист не остановится и увидит в вышеобозначенном свойстве следующее: множитель присутствует во всех слагаемых начиная с i-того. Это означает, что для для каждого можно взять слагаемых справа, заключить их в скобки и вынести множитель за скобки. Каждая такая группировка с выносом будет давать выражение . При этом, так как в слагаемом каждый каждый множитель присутствует лишь один раз, а выносу подверглись все множители со значением от 1 до n, от n-го слагаемого после выноса всех сомножителей останется лишь единица.
Для примера, при :
Можно для наглядности привести это выражений в RPN:- Код: Выделить всё
[0] 1 + 4 * 1 + 3 * 1 + 2 * 1 + 1 *
Отсюда хорошо видно, что для вычисления этого выражения стековой машине потребуется лишь один дополнительный фрейм стека. Это видно из того, что операторы и операнды строго чередуются. Фактически, это означает, что для вычисления данного выражения, хоть оно и имеет вложенных неатомарных подвыражений, нам хватит цикла с одной переменной, кроме переменной самого цикла. Ленивый, если и дойдёт до этого места, поленится подумать, и будет вычислять выражение рекурсией...
Таким образом, у думающего программиста код будет такой же, как и у не ленящегося, но без одной лишней переменной:- Код: Выделить всё
sum = 1
For i = n To 2 Step -1
sum = sum * i + 1
Next i
А сишники смогут поразить вас ещё и компактностью: for(sum = 1, i = n; i > 1; (sum *= i--)++);
Полученный вариант является тем самым минимумом: в нём выполняется минимально возможное число операций и используется минимальное число переменных. Дальшеоптимизироватьупрощать уже некудавыкидывать лишний хлам невозможно, потому что его не осталось.
Причина, по которой некоторые программисты делают меньше, чем полагается, предпочитая, чтобы программа делала больше, чем полагается, примерно такова: программист слишком любит себя, жалеет, а поэтому отказывается делать больше, чем делает, мотивируя это тем, что эту работу ему не оплачивают, а у него ещё жена, сын или дочка (нужное подчеркнуть), требующие не меньше внимания, чем код. Никто не спорит, что супруга и дети заслуживают внимания, но их упоминание как причины — чистейшей воды эгоцентризм. Потому что у будущих пользователей программы тоже есть супруг(а) и дети, и будущие пользователи предпочитают (не меньше программиста) уделить внимания семье, вместо того, чтобы задерживаться допоздна на работе, чтобы отправить проклятый никак не отправляющийся (по вине угадайте кого) или очень медленно отправляющийся отчёт или налоговую декларацию. То, что программисту нужно поработать один раз, а программа будет затем работать долгие годы, то, что страдалец-программист один, а страдальцев среди пользователей несколько больше, — эти факты не способны сломить эгоцентрическую позицию ленивого программиста.
Мораль: лень очень важна в создании качественного ПО. Главное, чтобы она поселилась в логике работы ПО, а не в логике его создателя.
P.S. Настоящие мастера практически никогда не занимаются оптимизацией своего кода: они просто не задумываясь пишут изначально оптимальный код.
Уточнение
Описанные примеры и рекоммендации не актуальны для языков, которые «исповедуют» функциональную (а не императивную) парадигму. Там даже такое явление, как ленивые вычисления имеется, поэтому думать о том, не выполняются ли лишние действия программисту не нужно, так же как и думать, какие вообще действия выполняются — специфика парадигмы.