Jump или не jump
[EN]

Так jump или не jump?

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

А если серьезно — существуют ли против этого действительно серьезные возражения, и есть хоть какие-то основания объявлять это "дурным стилем"? Конечно, легко написать совершенно идиотскую программу, пестрящую метками, расставленными "от фонаря". Но в любом мало-мальски нетривиальном случае можно точно так же, при желании, написать абсолютно невразумительный — но идеально модульный код, без единого перехода по метке. Принято ругать конструкцию goto за то, что она предположительно позволяет обойти нормальные процедуры закрытия блока; но это, скорее, вопрос некорректной компиляции, а не дефект самого кода. Адепты структурного программирования утверждают, что переходы по метке делают структуру программы недостаточно прозрачной, что при этом теряется явное отделение одной логически замкнутой структурной единицы от другой; однако большинство таких высказываний предполагает, что мы должны ограничиться лишь некоторым узким классом простейших структур (и стать чем-то вроде программистов-веганов). Можно также услышать высокопарные заявления о том, что использование goto несовместимо с крупномасштабным индустриальным программированием — что звучит, по меньшей мере, странно, поскольку условные и безусловные переходы используются в промышленных приложениях испокон веком, и это никогда особо не мешало работе. С другой стороны, почему мы должны ориентироваться только на массовую индустрию? Искусство программирования — не менее важный компонент человеческой культуры.

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

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

При структурном подходе, программа представляется последовательностью блоков A, B, ... , — причем каждый блок логически замкнут: он запускается некоторым событием и выполняется как целое, как одна простая операция. То есть, все возможные способы организации выполнения эффективно сводятся к простому условному выражению:

if e1 A;
else if e2 B;
...

Разумеется, в реальной жизни эта конструкция существует в разных вариантах. Так, можно задать особые обработчики, зарегистрировать их в системе и настроить прослушивание событий e1, e2, ... Это позволяет перейти от последовательной обработки к параллельной — что для независимых событий означает также изменений способа переключения. Но структура в целом та же. И в "плохо структурированном" коде можно было бы встретить нечто вроде

on e1 goto branch_e1;
on e2 goto branch_e2;
...
goto Exit;

branch_e1:
  A;
  goto Exit;
branch_e2:
  B;
  goto Exit;
...

Exit:

Какой уродливой ни показалась бы такая запись ригористу, структурная логика всегда может быть в точности воспроизведена с использованием goto (хотя бы потому, что на уровне оборудования она реализована именно так); более того, ничто не мешает нам подчеркнуть, при необходимости, общую структуру разного рода условными обозначениями и форматированием кода (отступы, именование меток, расположение фрагментов). Хороший компилятор должен уметь распараллеливать такую запись, по возможности автоматически определяя обработчики соответствующих событий и генерируя код очистки, выполняемый перед ветвлением. Для не слишком умных трансляторов программист может руками прописывать все необходимое (что иногда даже лучше). В любом случае, программный контроль ветвления намного превосходит структурный метод в гибкости, когда речь заходит о структурированных событиях. Например, если событие e1 предполагает событие ek, но не наоборот, можно просто написать:

on e1 goto branch_e1;
on e2 goto branch_e2;
...
on ek goto branch_ek;
...
goto Exit;

branch_e1:
  A;
  goto branch_ek;
branch_e2:
  B;
  goto Exit;
...
branch_ek:
  K;
  goto Exit;
...

Exit:

В "структурном" стиле придется дублировать код, генерировать дополнительные события, организовывать структурированную очередь событий — или еще что-нибудь столь же неуклюжее. По нынешней моде, можно было бы "обернуть" исходные блоки в функции и заменить прямую передачу управления блоку A блоком вида {call fA;} — это эффективно порождает иерархию вложенных блоков (или рекурсивные функции). Такая технология позволяет комбинировать блоки с минимальными накладными расходами: {call fA; call fK;}; однако не похоже ли это больше на замаскированную комбинацию ветвлений? По сравнению с подобными уродливыми монстрами, простое goto смотрится куда изящнее и делает логику переходов совершенно прозрачной, так что изменить при случае структуру событий не представляет труда.

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

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

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


[Компьютеры] [Наука] [Унизм]