Программирование. От главного — к ремеслу

Современный новичок, пытающийся освоить программирование, попадает в незавидную ситуацию по сравнению со своими предшественниками, изучавшими радиотехнику и электронику. Замечательным книгам Айсберга (из серии "… это просто") "программистских" аналогов нет. И, казалось бы, таковые невозможны — транзистор, он, как говорится, и в Африке транзистор, а в программировании существуют несколько тысяч языков высокого уровня, тысячи машинных архитектур со своими низкоуровневыми языками, сотни методологий. Впрочем, есть и "весомо, грубо, зримо" прорвавшиеся через годы и ставшие международными стандартами технологии, определяющие почти не зависимое от "ремесленных" тонкостей направление, при движении по которому новичок быстро освоит "самое главное". У этих технологий-стандартов есть и конкретные "имена" (DIN 66261 и ISO/IEC 8631:1989), и создатели. Так что давайте заранее поблагодарим двух замечательных ученых — Бена Шнайдермана (Ben Shneiderman, профессор Университета Мериленд) и Айка Насси (Ike Nassi, сегодня — один из директоров Cisco) за их статью, увидевшую свет в 1973 г., с которой все и началось…

Карандаш и бумага

Существует подтвержденное практикой правило, что очень хорошие программисты вообще не работают с компьютерами, предпочитая общаться не с "тупой от рождения" машиной, а с людьми: именно программисты экстра-класса создают архитектуры и спецификации программ/данных, оставляя черновую, рутинную и ремесленную работу специалистам классом пониже. Учиться надо у профессионалов — и мы теперь знаем, что программирование начинается с карандаша и бумаги. Это и будет "самый виртуальный" компьютер, представляющий в наше распоряжение "самый универсальный" язык программирования. Причем не просто универсальный (здесь кавычки уже неуместны — дальше будет понятно, почему), но и гарантирующий отменные "читабельность", стиль и структуру программ, портированных из "виртуально-бумажной" среды на "настоящие" компьютеры. Короче говоря — все то, что всегда считалось признаком "хорошего тона" в профессиональном программировании (увы, принятая системой обучения модель, при которой новички вынуждены долго "барахтаться" в какой-либо конкретной системе программирования, ничего подобного не гарантирует).

От tabula rasa

Все, что пока вам необходимо, уже есть под рукой — бумага и карандаш. Теперь пора вооружиться минимальным перечнем сведений и освоить азы терминологии.

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

Итак, у нас в распоряжении есть любые предметы и абстракции, которые требуются нашей прикладной областью, — буквы, цифры, паровозы и даже, если хотите, Чебурашки. Всех их объединяют общее название — данные, — и множество операций, которые над ними можно производить (даже Чебурашек, например, можно сосчитать). Частные названия для данных принято именовать "типами" — скажем, "цифра" или "Чебурашка" — это типы. Данным надо где-то находиться, причем некоторые из них удобно иметь "под рукой", и для всех нужно предусмотреть возможность длительного или вечного хранения. Попытаемся сначала ответить на вопрос "Под чьей рукой?". Действительно, даже "самый виртуальный компьютер" не подозревает о нашем существовании, так что уж точно речь идет не о наших руках. Представим себе безукоризненно послушных нашей воле, но, увы, совершенно безмозглых многоруких существ и назовем их процессами. Все, что умеют делать процессы, — это точно выполнять наши команды (то, что мы называем программой), перекладывая своими "руками" данные с места на место и производя над этими данными допустимые операции. Теперь понятен ответ на вызвавший отступление вопрос — данные должны быть "под руками" процессов. "Ящики", в которых процессы хранят данные, называются переменными, причем "форма" этих "ящиков" подогнана под "форму типа данных", и разместить в ящике, предназначенном для хранения чисел, например, Чебурашку, не удастся. Так как "ящиков" может быть много, то для них придумывают названия (имена переменных). Естественно, раз переменные должны быть "под руками" процессов, то для упомянутого удобства "ящики" должны располагаться "ближе, чем кожа", т. е. внутри процессов. И не страшно, что у наших процессов руки растут "внутрь"=— такие странные законы в мире "самого виртуального компьютера".

Зато для длительного хранения данных есть прекрасно соответствующие реальности "кладовки", куда можно складывать что попало и которые существуют всегда, — эти "кладовки" именуются в реальном мире файлами (к слову, этот термин не кажется удачным из-за слишком яркого "офисного" характера). У "кладовок", естественно, есть "двери", и для того чтобы добраться до их содержимого, эти "двери" надо "открыть" (разумеется, правила хорошего тона и безопасности требуют, чтобы у "кладовок", данные из которых не нужны, "двери" были "закрыты"). Из открытой "кладовки" можно "брать" данные и помещать их в "ящики"-переменные (больше ведь некуда), и в нее можно "укладывать" данные из "ящиков"-переменных. В настоящих компьютерах процедурам "открывания/закрывания кладовок" и "взятия/укладки данных" соответствуют процедуры открытия/закрытия и чтения/записи файлов.

Странная логика мира "самого виртуального компьютера" уже должна натолкнуть внимательного читателя на одно очевидное несоответствие: если у процессов, управляемых нашими программами, "руки растут внутрь" и оперировать они могут только данными из расположенных внутри них "ящиков"-переменных, то как процессы "дотягиваются" до находящихся вне них "кладовок-файлов"? Все правильно=— сами процессы никак не могут этого сделать. И вот настало время еще одного персонажа — Самого Главного Процесса, иначе говоря, операционной системы. Дело в том, что все остальные процессы находятся "внутри нее", и только она может передавать "на время" "ящики"-переменные процессам (в них, например, могут находиться данные о текущем времени, такие переменные называются "временными") или же сама "укладывать" данные в "ящики"-переменные процессов (обычно так происходит при чтении данных из файла).

Теперь последние штрихи… Сами процессы не могут браться "из ниоткуда" — они "спят" в "кладовках"-файлах, и по нашему указанию или по требованию находящихся "внутри ОС" процессов (они именуются активными) ОС может размещать новые процессы "внутри себя" (в настоящих компьютерах этот процесс называется запуском). Для предъявления ОС требований от активных процессов существует механизм "системных вызовов" — множество "рук", протянутых ОС внутрь каждого активного процесса. Для того чтобы запросить, например, данные из предварительно открытой "кладовки" (чтение из файла), процесс должен приготовить "ящик"-переменную для полученных данных и "дернуть ОС за соответствующую руку" (осуществить системный вызов).

В принципе, такой забавной "модели мира" уже достаточно. Реальные системы отличаются совершенно неважными для сути программирования деталями — в них ОС, кроме основных системных вызовов (открытия/закрытия, чтения/записи файла, запуска/остановки процесса), предлагают мешанину из "всего понемногу" — в том числе и вызовы, обеспечивающие доступ к аппаратным средствам компьютера. Множество системных вызовов растет по мере усложнения аппаратуры, системы становится труднее изучать, соответственно, для них становится труднее писать программы. Именно поэтому Unix и, особенно, Plan9 — ОС с развитой и единой концепцией файла ("все, что есть, — это файлы")=— заслуженно считаются исключительно удобными для программирования (например, в Plan9 вместо изучения сложных программных интерфейсов для доступа к встроенным в компьютер аппаратным часам, программист просто открывает файл "часы" и читает его содержимое). Возможно, пока речь идет об одном системном вызове, это утверждение не кажется убедительным, но в реальных ОС системные вызовы исчисляются сотнями и даже тысячами…

Покомандуем

Выше уже не зря упоминалось, что процессы, несмотря на "многорукость", умом и сообразительностью не отличаются. Они "понимают" только самый примитивный графический язык, "фразы" которого очень легко рисовать, благодаря Насси и Шнайдерману. Именно этот язык возведен в ранг всемирного стандарта ISO/IEC 8631:1989 (естественно, что полностью придерживаться стандарта мы не будем, более того, позволим себе некоторые вольности, совершенно не сказывающиеся на качестве "конечного продукта").

Не слишком умный процесс можно заставить выполнять простую последовательность действий, например в последовательные моменты времени — некоторые действия A, B, C, D, … (рис. 1, а).

Как видите, ничего страшного — то, что находится в каждом прямоугольнике, именуется действием.

Более сложная "фраза" — ветвление (рис. 1, б). Действие A в нем играет ключевую роль — оно определяет, какая из ветвей-последовательностей (Bl, Cl, Dl, … или Br, Cr, Dr, …) будет выполняться. Естественно, если A не изменяет своего значения в ходе "выполнения" нашей "программы", весь смысл конструкции ветвления теряется — мы можем заменить ее последовательностью. Отсюда вытекает требование к характеру действия A — это должно быть высказывание, в ходе выполнения определяющее одно из двух значений (именно двух — ведь диаграмма ветвления предусматривает выбор из двух последовательностей). Такая строгость требования буквально вынудила ввести даже специальные термины для значений, определяемых A, — true (истина, да) и false (ложь, нет). Или коротко — 1 и 0. Высказывание обычно бывает проверкой, например, "равно ли содержимое "ящика"-переменной с именем P1 содержимому "ящика"-переменной с именем P2?". В зависимости от результата (да/нет) будет выполняться соответствующая "ветвь программы".

А как быть, если выбор не так прост — не только "да/нет"? Для таких случаев есть подходящая конструкция — селектор (рис. 1, в). Он определяет несколько (более двух) возможных "потоков исполнения". Селектор также предъявляет жесткие требования к действию SN — мы должны точно знать хотя бы несколько значений, которыми может характеризоваться SN в ходе исполнения "программы". Несмотря на очевидность графической нотации, принцип работы селектора удачно объясняется с помощью слова "когда":

выполнить действие Z, когда
SN характеризуется значением A
выполнить действие Y, когда SN характеризуется значением B
выполнить действие X, когда SN характеризуется значением C
выполнить действие W, когда SN характеризуется значением D

У неумных процессов есть одно замечательное достоинство — работоспособность. Человеку трудно выполнять монотонно повторяющиеся операции, для процесса же это излюбленное дело. Соответственно, в нотации Насси-Шнайдермана повторяющимся операциям уделено заслуженное внимание — целых две конструкции (рис. 1, г). Обе они описываются фразой "повторять действия A, B, C, …, Z до тех пор, пока S характеризуется как true". Но есть один очень важный момент — проверка этого самого S в первой конструкции осуществляется перед выполнением действий A—Z (соответственно, если изначально S характеризуется false, то ни одно действие выполнено не будет), а во второй — в конце (и даже если S изначально — false, то обязательно один раз будет выполнена вся последовательность действий A—Z).

Вот, собственно, и все, что может понимать процесс. Дальше начинается уже самое главное в программировании — искусство формирования алгоритма.

Первый блин

Теперь давайте попробуем написать первую "программу" для нашего "самого виртуального компьютера". Причем настоящую программу. И пусть она автоматизирует, возможно, очень нужный кому-нибудь, но без сомнения трудоемкий процесс подсчета сочетаний букв "na", "Na" и "sh", "Sh" (как в словах nation, Nassi, shield, Shneiderman) в файле, содержащем английский текст.

Итак, последовательность основных действий очевидна: раз речь зашла о файле-"кладовке", то прежде всего его надо открыть. Затем выполнить то, что нам нужно (пока мы не знаем как), и, по правилам хорошего тона, закрыть файл. В нотации Насси-Шнайдермана наша первая программа изображена на рис. 1, д.


Теперь об основной части — сначала надо попытаться представить процесс подсчета
сочетаний букв и сообразить, сколько и каких "ящиков"-переменных нам
понадобится. Зная, что из файла можно прочитать что угодно, например букву, и
поместить прочтенное в "ящик"-переменную (чтобы наш процесс мог ею воспользоваться),
зная управляющие конструкции, понятные процессу, попробуем мысленно представить
себя на его месте — как бы мы, обладая такими возможностями, решали эту задачу.

Итак, нам нужны конечные результаты — количество сочетаний букв. Естественно, что значение этих результатов для некоторого файла заранее неизвестно, и в ходе "изучения" файла будет изменяться. Переменный характер результатов и необходимость их хранения от начала до конца выполнения программы подсказывают, что они должны быть переменными-"ящиками". Простейшая логика опять же помогает определиться с их типами=— это должны быть числа. Имена для ящиков лучше всего выбрать не мудрствуя лукаво — Na_counter, Sh_counter (маленькое полезное универсальное правило=— в именах "ящиков" можно использовать только английские буквы и символ "_").

Учитывая это соображение, без дальнейшего "разжевывания" можно привести
готовую "программу" для нашего "виртуального компьютера" (рис.
2
). Теперь настало время вам самим проявить сообразительность и попытаться
решить эту же задачку другими способами — в программировании не бывает задач,
решающихся "одним способом".

Сам себе… транслятор

Трансляторы, как известно, — программы, преобразующие другие программы. В нашем случае ценность полученной выше "программы" является ненулевой даже без трансляции в реальный язык программирования — это хороший образец высокоуровневой документации (для которой и созданы упомянутые выше стандарты), несмотря на наивность определений и формы. И, кроме того, это последний этап процесса программирования, на котором заканчивается искусство. Дальше начинается ремесло — тяжелое и кропотливое.

На каждое действие с файлами, процессами и прочими объектами, находящимися "извне" вашего будущего процесса, надо найти в документации по ОС или вспомогательным библиотекам соответствующие системные вызовы — если нет именно таких, какие вам нужны, всегда можно создать имитацию необходимого и для этого переделать нотацию. Затем следует изучить конкретный язык программирования и отыскать в нем эквиваленты конструкциям нотации Насси-Шнайдермана (в любом языке таковые имеются). Например, для популярного С существуют точные эквиваленты (рис. 3).

Вот так, шаг за шагом изучая ОС и язык программирования, увеличивая детализацию
вашей диаграммы Насси-Шнайдермана, вы непременно придете к такой точности описания
"программы", когда кавычки у этого слова придется убрать. Причем придете
единственно правильным путем — от Программирования к программированию. Дальше
этот путь неизбежно приводит к пониманию необходимости совершенствования мастерства
— к освоению уже разработанных и изученных алгоритмов, для этого существуют замечательные,
ставшие классическими книги Никласа Вирта и Дональда Кнута.