Нелокальные управляющие конструкции считать вредными
От: Tilir Россия http://tilir.livejournal.com
Дата: 22.03.13 09:42
Оценка: 48 (5) +3 -6 :))) :))
Дейкстра в классической статье [1] приводит несколько серьёзных доводов против нелокального goto. В частности, он пишет: "наши интеллектуальные возможности в основном направлены на анализ статических отношений и, напротив, наши способности представлять ход процессов, разворачивающихся во времени, развиты сравнительно слабо. Поэтому мы должны (как умные программисты, осознающие, собственные ограничения) делать всё от нас зависящее, чтобы уменьшать концептуальный зазор между статическим кодом программы и динамическим процессом её выполнения, добиваясь как можно более тривиального соответствия между программой (разворачивающейся в виде текста) и порождённым ей процессом (разворачивающимся во времени)".

Очень похожие слова мы можем встретить в современном документе Google C++ Style Guide [2] но там они касаются уже исключений (C++ exceptions): "исключения делают поток управления программы сложным для оценки при просмотре кода: функции могут вернуть управление в неожиданных местах. Вы можете минимизировать это, соблюдая некие правило относительно того как и где можно использовать исключения, но ценой этого будет то, что разработчик должен будет помнить и знать гораздо больше, чем ему нужно [для работы над его проблемой]". Такую же критику исключений приводит Джоэль Спольски [3] добавляя также "невидимость в исходном коде" и отмечая, что "даже тщательный анализ кода в этих случаях не помогает выявить проблемы".

Очень похожие аргументы о недетерминированном поведении кода приводит Edward A. Lee из Berkeley в своей работе [4], посвященной критике нитей исполнения (threads). Он пишет: "мы должны (и можем) строить детерминированные модели параллельного исполнения, в которые обоснованно и осторожно вносить недетермнизм там, где он реально нужен. Нити исполнения предлагают противоположный подход. Они делают программы абсурдно недетерминистичными и сильно полагаются на хороший стиль программирования, чтобы ограничить этот недетерминизм и достичь детерминированных целей"

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

* Нелокальный GOTO
* Continuations
* Исключения
* Сигналы
* Нотификации
* Сопрограммы
* Генераторы
* Нити исполнения
... имя им легион ...

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

Сложность жизненного цикла программы это не только сложность разработки, но и сложность поддержки. Большинство признанных сегодня методов разработки, таких как структурное программирование, ООП и паттерны проектирования, etc направлены на борьбу со сложностью и делают разрабатываемые системы более прозрачными, более соответствующими Principle Of Least Astonishment [5], и в конечном итоге более простыми в поддержке. В противовес этому, нелокальные управляющие конструкции дают прирост скорости разработки за счёт потери детерминированности и простоты разрабатываемого приложения.

Эрик Рэймонд в "Искусстве программирования под Unix" пишет: "Программную систему можно назвать прозрачной, если в ней нет тёмных углов и скрытых глубин. Прозрачность это пассивная характеристика. Программа прозрачна когда вы можете сформировать простую умственную модель её поведения, которая действительно предскажет все или большинство случаев работы с программой, потому что вы можете через всю сложность реализации, тем не менее понимать, что происходит".

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

В функциональных языках нелокальные конструкции, такие как pattern matching или ленивые списки, не вызывают особой боли, потому что являются естественной частью non-modifyable мира, где модификация загнана внутрь монад и интеллектуальные ресурсы разработчика не расходуются на поддержку консистентности модифицируемых структур данных при нелокальных переходах. Но по многим причинам (в частности из-за высокого входного порога и принципиально низкой производительности на существующих архитектурах -- представьте, например, что вместо модификации одного узла дерева, вы полностью уничтожаете и создаёте заново весь миллион его узлов), такие языки до сих пор представляют скорее академический интерес. Что касается нелокальных конструкций в модифицируемом мире, то мы должны отказаться либо от них, либо от оператора присваивания. К тому, же в мире чисто функционального программирования, детерминированности остаётся ещё меньше.

В плане производительности и детерминированности, следует также отметить, что очень часто их использование сопряжено с существенным и невидимым (или сложно оцениваемым) оверхедом. Например Glodstein, Rowe и Kesden из Карнеги-Меллон [6] особо предупреждают о существенном оверхеде при использовании сигналов Unix и переключении контекста процессов.

В отчёте С++ performance technical report [7], про исключения аккуратно сказано: "для некоторых программ проблему составляет сложность в предсказании времени, требующегося для передачи управления от throw до подходящего catch" и предлагается провести для каждой точки возможного выброса исключений сложный анализ, включающий анализ времени работы всех деструкторов объектов, которые могут быть вызваны при раскрутке стека, который позволит установить верхнюю и нижнюю границу времени выполнения. Это очень типичный аргумент для всех адептов нелокального счастья -- они советуют писать код с использованием всех их прекрасных возможностей, а уже потом при отладке многопоточного приложения с исключениями и сопрограммами искать узкие места и думать над тем, откуда это счастье выкинуть. И это явно не инженерный подход.

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

Именно поэтому я предлагаю все нелокальные управляющие конструкции считать вредными.

Литература

1. http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html
2. https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions
3. http://www.joelonsoftware.com/items/2003/10/13.html
4. http://www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.html
5. http://c2.com/cgi/wiki?PrincipleOfLeastAstonishment
6. http://www.cs.cmu.edu/~213/lectures/14-signals.pdf
7. http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.