суббота, 14 июня 2014 г.

[Перевод] G.Weinberg - 1.1 Чтение программ


Когда-то, во времена популярности таких языков как COBOL, очень активно обсуждался вопрос о возможности менеджеров читать программы своих программистов. Как выяснилось позже, все эти действия были направлены просто на привлечение менеджерами дополнительных средств: они просто не хотели зависеть от своих программистов. О реальной возможности чтения программ речи не шло. Ведь, действительно, зачем менеджерам читать программы? Даже сами программисты их не читают.
Но разве есть реальная необходимость читать программы? Разве программы пишутся не для ЭВМ? И да, и нет. Даже закрывая глаза на то, что программы нужно модифицировать, создавать им интерфейс для общения друг с другом и т.п., идея читать программный код не такая уж и плохая с точки зрения обучения программированию.
Программирование, среди всего прочего, это своего рода письменность. Чтобы освоить письменность, нужно, в первую очередь, писать. Но, что касается других форм письменности, чтение также способствует освоению этого навыка. А может ли программист научиться писать программу, просто почитывая программный код? Теоретически — да, практически — никогда. Что же полезного тогда можно извлечь из чтения программ? Давайте рассмотрим маленький пример на языке PL/1 (см. Пример 1).

Пример 1.
XXX: PROCEDURE OPTIONS(MAIN);
     DECLARE B(1000) FIXED (7,2),
             C FIXED (11,2),
             I,J FIXED BINARY;
     C = 0;
     DO I = 1 TO 10;
         GET LIST((B(J) DO J = 1 TO 1000));
         DO J = 1 TO 1000
             C = C + B(J);
             END;
         END;
     PUT LIST('SUM IS', C);
     END XXX;

Что полезного можно извлечь из чтения этого куска кода? Поскольку программный код — это не любимый роман, в котором понравившиеся моменты отмечены замятым уголком книги, нам нужны какие-то концептуальные основы, которые объясняли бы происхождение каждой строчки кода. Иными словами, когда мы смотрим на каждый фрагмент кода, мы задаем себе вопрос: «Почему этот фрагмент здесь появился?»

Машинные ограничения
Одна из причин появления какого-либо фрагмента кода — это ограничения ЭВМ, на которой решается данная конкретная задача в сравнении с некоторой идеальной ЭВМ. В предложенном примере видно, что в общей сложности была получена сумма десяти тысяч чисел, но прочитаны они были в циклах по тысяче. Поскольку числа, очевидно, перфорированы1 в формате списков PL/1, то единственная причина такого решения — невозможность сохранить все 10 000 чисел одновременно. Другими словами, не имея 40 000 байтов в наличии, программист должен разбить свою процедуру суммирования на маленькие "секции", что привело к появлению дополнительного цикла. Если бы ЭВМ была в состоянии хранить все 10 000 чисел, то, вероятно, код программы выглядел бы так, как показано в примере 2.

Пример 2.
XXX: PROCEDURE OPTIONS(MAIN);
     DECLARE A(10000) FIXED (7,2),
             C FIXED (11,2),
             J FIXED BINARY;
     C = 0;
     GET LIST((A(J) DO J = 1 TO 10000));
     DO J = 1 TO 1000
         C = C + A(J);
         END;
     PUT LIST('SUM IS', C);
     END XXX;

Разумеется, когда программист пишет подобный фрагмент кода,предназначенный для преодоления машинных ограничений, за редким исключением это будет отражено в комментариях. Хотя это и добавляет интриги чтению программ, всё же такие действия плохо сказываются на программе, когда, например, она передается в работу на другую машину.
Ещё одна область, вынуждающая программистов писать код с учётом машинных ограничений, это потребность в промежуточном хранении. Эта потребность существует потому, что идеальный и супернадежный носитель информации ещё не создан, поэтому программисту приходится иметь дело со множеством различных устройств хранения, каждое из которых имеет свои особенности выбора времени и способов обращения, вместимости. Таким образом, значительная часть работы программиста направлена на преодоление разнородности в хранении информации.

Ограничения языка
Однако, машинные ограничения нас мало интересуют, ведь мы в вопросах психологии ищем более "человечные" причины написания того или иного кода. Шагом навстречу им будет рассмотрения роли языковых ограничений в приведенном нами Примере 2.
Вообще говоря, язык PL/I предоставляет программисту встроенную функцию под название SUM для вычисления суммы элементов множества. Изначально функция SUM была больше «математической» функцией, чем «арифметической»: она принимала входящие значения в формате с плавающей запятой или преобразовывала их к такому формату при необходимости. Однако, такое преобразование могло привести к потери точности, если бы массив состоял из десятичных дробей с фиксированной запятой (FIXED DECIMAL). Многие программисты было обеспокоены обнаружением потери пени, при попытке использования функции SUM для расчёта бухгалтерских балансов.
Возникшая ситуация привела к тому, что функцию SUM объявили как непригодную для точных расчётов, т.е. как чисто арифметическую. Если бы не это языковое ограничение,то программа из нашего примера могла бы выглядеть лаконичнее, как показано в Примере 3.

Пример 3.
XXX: PROCEDURE;
     DECLARE A(10000) FIXED (7,2),
             J FIXED BINARY;
     GET LIST((A(J) DO J = 1 TO 10000));
     PUT LIST('SUM IS', SUM(A));
     END XXX;

В примере 3, мы также убрали OPTIONS(MAIN) при объявлении PROCEDURE, иллюстрируя другую форму языкового ограничения - ограничение конкретной реализации. Этот неуклюжий атрибут требовался только в определенных операционных системах, интерфейс которых требовал, чтобы головные MAIN-программы обрабатывались иначе, чем подпрограммы.

Ограничения программиста
Чисто психологическими являются ограничения самого программиста, который по каким-то причинам недостаточно хорошо владеет языком, компьютером или самим собой. В нашем примере, программист действительно не понимал обозначения массива в PL/I - до такой степени, что даже не осознавал возможности поместить имя массива в список данных, чтобы получить все его элементы. Если бы он был знаком с этой особенностью языка (которая была, в конце концов, даже в FORTRAN) он, возможно, написал бы программу, показанную в Примере 4.

Пример 4.
XXX: PROCEDURE;
     DECLARE A(10000) FIXED (7,2);
     GET LIST(A);
     PUT LIST('SUM IS', SUM(A));
     END XXX;

Незнания полной мощности используемого языка — это так называемые лексические ограничения.

Историческое наследие (legacy-код)
Очень часто в программах можно обнаружить код, классифицируемый по любому из вышеуказанных ограничений, но доставшийся программе "в наследство". Например, как только функция SUM превратилась в арифметическую функцию, то не осталось ни одной причины использовать её для точных расчётов, как в Примере 2. Тем не менее, если эта программа уже находится в состоянии промышленной эксплуатации, то маловероятно,что кто-нибудь начнет в ней ковыряться только из-за того, что изменилось определение функции SUM.

Однажды два программиста, разбиравшиеся в коде одной программы, работавшей в управлении социальной безопасностью, обнаружили любопытный артефакт. Каждый раз, когда у входной перфокарты в определённой колонке находилась буква «А», она преобразовывалась в «1». Это было особенно любопытно ввиду того, что в этой колонке фактически не могло быть чисел и программа была написана так, чтобы гарантировать их непопадание. Отказавшись переписывать всё заново, программисты начали расследование, которое выявило следующее.
Несколькими годами ранее, на одном из клавишных перфораторов в одном из окружных офисов появилась ошибка, которая вызывала пробой «А как 1» именно в этой колонке. Так как это было перед предварительной отладкой программы, эти неправильно перфорированные карты проходили через внутреннюю программу и аварийно завершали её. К тому времени, когда проблема была обнаружена, было неизвестно, сколько таких карт в обращении. Таким образом, самым простым выходом из этой ситуации стала «временная» модификация к программе. Как только она была сделана, карта вновь заработала хорошо, и все забыли об этом (снова психология!). А баг сидел там до тех пор, пока не оказался раскопан много лет спустя двумя программистами-археологами.

К сожалению, не весь исторический код может быть так легко распознан, как это показано в наших примерах. В частности, чем больше программа, тем более разбросанными оказываются последствия исторического выбора, сделанные в начале её "жизни". Даже сама структура программы может быть определена размером и составом группы программистов, которая первоначально занималась её разработкой, так как работа должна была быть разделена между определенным числом людей, у каждого из которых были определенные достоинства и недостатки.

Требования
Рассматривая Примеры 1 и 4, можно подумать, что решением самой задачи занимается довольно малая часть всего написанного кода. Довольно справедливое замечание для приведенных примеров, однако, не исключающее наличие программ, занимающихся конкретно только решением задачи.
И всё же, даже успешно извлекая полезное "ядро" программы из приведённых примеров, не стоит впадать в иллюзию, что при разработке всё так очевидно и просто: сначала решается задача, а якобы потом достраивается код, преодолевающий всевозможные ограничения. Кроме очевидных трудностей в определении намерений программиста, таких как незнание языка или написание "преждевременно оптимизированного" кода, всегда будет оставаться фактом то, что в большинстве случаев мы не знаем, что мы получим, пока не "сядем в лужу", начав это программировать.
Требования развиваются вместе с программами и программистами. Написание программы является процессом обучения и для программиста и для заказчика. Кроме того, этот процесс обучения имеет место в контексте определенной машины, определенного языка программирования, определенного программиста или программной команды в особых производственных условия, и определенном наборе исторических событий, которые определяют не только форму кода, но также и то, что делает код. В некотором смысле, самая важная причина изучения процесса программирования заключается не в том, чтобы сделать программы более эффективными, компактными, дешевыми или более понятными. Самая важная выгода - это перспектива получения от наших программ того, что мы действительно от них хотим, а не того, что нам удастся выжать из них всеми силами.

Подводя итог, можно сказать,что есть множество причин, почему программы пишутся так или иначе. Когда мы читаем код, мы видим, что какая-то часть его написана из-за машинных ограничений, какая-то из-за языковых, какая-то отражает ограниченность самого автора, а какая-то просто досталась в наследство. Но по какой бы причине не была написана отдельная часть кода, всегда существует психологический аспект этой причины, что приводит нас к мысли о том, что изучение программирования как человеческого поведения будет иметь многочисленные и не всегда ожидаемые плоды.



1 - речь идёт о программах на перфокартах.

2 комментария:

  1. Спасибо за перевод!
    Давно подбираюсь к Вайнбергу, но никак не доберусь. У буржуев он действительно считается классиком и особенно часто на него ссылаются тестеры.

    ОтветитьУдалить
  2. Кирилл Алексеев7 июня 2015 г., 12:32

    Спасибо за отзыв! Работу по переводу планирую продолжить, но пока времени маловато, к сожалению.

    ОтветитьУдалить