суббота, 20 октября 2007 г.

Простота и сложность (2)

Позавчера пришел такой комментарий:

> Для их создания приходится использовать адекватные инструменты, которые не могут не соответствовать сложности и ответственности задач и потому объективно не могут быть простыми. (это был мой текст)

Ну а если говорить в плоскости Java и C++? Первый язык значительно проще плюсов, и, при этом, на нем решаются действительно сложные задачи. В принципе, на том же Обероне Вирт реализовал реально работающую ОС -- проект отнюдь не маленький. Тот же Страуструп часто говорит, что язык, в конечном счете, вторичен, это всего лишь инструмент.

Кстати, с большим интересом прочитал бы ваше мнения о Java и C# как о языках (в некотором отрыве от виртуальной машины и фреймворка).

Во-первых, я не хотел бы «сваливаться» в неформальное сравнение языков (обещание себе дал :-). Если уж сравнивать, то глубоко, предельно конкретно (по существу), тщательно и непредвзято. Для этого нужен традиционный формат журнальной статьи, причем в журнал уровня Software: Practice & Experience или по крайней мере C++ User’s Journal, но уж конечно, не заметка в блог. (Кстати, что-то не припомню подобного сравнительного анализа!- по большей части, встречал только что-то вроде «Critique of C++» или известные едкие пассажи про «Жабу»...)

Тем не менее, кое-что сказать можно. Конечно, Java проще и стройней (и чисто по-человечески – мне, по крайней мере - писать на ней, как и на C#, не в пример удобнее и приятнее, чем на плюсах). Что же касается решения сложных задач, то их ведь решают на всех языках – и на чистом Си, и на Обероне, в том числе. Я лично знаю человека, который один написал больше полумиллиона строк на ассемблере БЭСМ-6 - и его программа была вполне рабочей, даже популярной - несколько десятков инсталляций по бывшему Советскому Союзу.

Вопрос, в конечном счете, заключается в том, какова цена решения, основанного на том или ином языке: какова вероятность завершения работы в заданный срок, насколько такое решение надежно, масштабируемо, развиваемо, сопровождаемо – прежде всего, в долговременном плане. Если брать в рассмотрение эти критерии, то мне все-таки кажется, что «простые» языки... ну, скажем мягко, не выглядят полностью адекватными им (этим критериям).

Насчет вторичности языка. Язык, конечно, вторичен, однако встык к Вашей цитате из Страуструпа я бы поставил цитату из Дейкстры (под рукой нет первоисточника, передаю смысл): «язык – это инструмент, но это такой инструмент, который оказывает глубокое (и тонкое!) влияние на мышление того, кто им пользуется». Рискну проиллюстрировать тезис классика вполне тривиальным примером.

Пусть вам нужно запрограммировать группу логически связанных действий, которые работают над некими общими для них данными. В простом Си у вас нет другого способа реализовать эти действия, кроме как определить данные в виде глобальных переменных (или структуры, неважно), а действия представить в виде простого набора функций. Если нужно как-то обособить этот код от других частей программы (уж не говорю – оформить его как независимый модуль!), то имеется, по существу, единственное решение -вырезать этот код (ножницами :-)) из программы, положить его в отдельный файл, а для «склейки» программы использовать текстовые инклюды. Написаны десятки статей, объясняющих недостатки и опасности такого решения, не буду повторять их,- но ведь в Си ничего другого нет! Ну, в крайнем случае можно попытаться откомпилировать этот фрагмент отдельно от основной программы, а потом прилинковать его; однако не всегда это возможно, а очень многие проблемы при этом остаются.

Так вот, значит ли это, что Си нельзя использовать при разработке сложных программ? Конечно, не значит!- используют же. Но в этом случае на объективную сложность решаемой задачи будут накладываться сложности, порожденные бедным набором выразительных средств и возможностей инструмента. То есть, язык не только не помогает«бороться со сложностью задачи», но еще и привносит в решение собственные проблемы... Получается, как и сказано у Дейкстры: инструмент оказывает (в данном случае негативное) влияние на мышление программиста по поводу его прграммы.

Давайте пойдем чуть дальше; заодно я отвечу на один недавний комментарий. Вот он:

AVC>Интересно, почему модульности очевидно недостаточно для борьбы со сложностью?

ЕЗ>Э-э-э... А что, достаточно? То есть, в языке достаточно иметь модули, и они одни способны решить все проблемы создания больших программ? Я что-то не понял реплики, простите.

Вопрос был не о всех проблемах создания больших систем, а конкретно о борьбе со сложностью. (Мне просто хотелось услышать обоснование. А то о Си++ Вы пишете много, а об Обероне только одно слово, что его очевидно недостаточно. :) )
Что касается борьбы со сложностью.
Возможно, я не в курсе, но мне известен только один способ борьбы со сложностью: делить сложное целое на части так, чтобы части не вмешивались в дела друг друга. Для этого нужны прежде всего границы, а границы и суть модули.
Интересно, что еще (кроме модулей) имеет отношение непосредственно к борьбе со сложностью?


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

Так вот, давайте тупо (с некоторым огрублением) сравним типичную структуру Си-программы и структуру Оберон-программы.

Си: линейная последовательность функций. Си-функция не может хранить собственный контекст между своими вызовами (это «stateless»-компонент), поэтому для обеспечения сколько-нибудь нетривиального взаимодействия приходится активно использовать глобальные переменные.

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

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

Возразят: в Обероне есть поддержка ООП, которая вводит новое качество: можно организовывать более сложные и многобразные схемы взаимодействия компонент, более удобно развивать систему, не подвергая опасным модификациям написанное ранее.

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

Можно сказать, что и Вирт, и авторы второй (1995 года) редакции Ады приняли одно и то же проектное решение: вводя в язык новые понятия (а в том, что касается создания сложных систем, ООП - принципиально новое понятие, пусть это было осознано не сразу), делали это, стараясь как можно меньше изменить концептуальный базис существующего языка. Они рассуждали примерно так (Вирт, по крайней мере, именно так): зачем вводить "классы", если уже есть записи?- давайте введем "расширяемые" записи (в Аде - "тегированные"). Смысл тот же, а число базовых понятий не увеличилось. Зачем "функции-члены"?- пусть останутся обычные подпрограммы, только сделаем их "связанными с типом". В Аде, кажется, вообще даже нового термина не ввели, обошлись словесными объяснениями, как подпрограммы должны работать с параметрами тегированных типов.

(Авторы Ады ужасно гордились, что они сделали свой язык объектно-ориентированным, добавив в него всего лишь шесть, кажется, новых служебных слов. Нашли чем гордиться: Вирт не добавил вообще ни одного...)

Такой подход кажется концептуально очень "чистым" и грамотным; он правилен и в практическом ключе: обеспечивается совместимость со старыми версиями языков. Однако, это решение оказалось очень ограниченным: "объектность" в обоих языках осталась "гражданином второго сорта", будучи всегда заключенной в рамки "более равных" программных единиц. Поэтому, между прочим, на обоих языках программировать объектно оказалось, попросту говоря, крайне неудобно. Как только потребности решаемой задачи выходят за рамки красивых и понятных примеров из руководств, начинается сущее мучение. В Аде, якобы, хотят как-то улучшить "юзабилити", предлагая нотацию вида "object.method", но для этого надо ждать нового стандарта...

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

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

Чтобы не упрекали в голом критиканстве, вот очень приблизительный список того, что хотелось бы иметь. Может, чего-то упустил, а что-то из перечисленного на самом деле и ни к чему - не судите строго, это все-таки не статья в журнал. :-)

Обобщая: различные виды модулей и развитые средства задания отношений между модулями. Скажем, в Аде можно делать вложенные модули (самый простой вид отношений), в последней Аде появились дочерние модули. В ней же есть задачи – модульные средства задания параллельного выполнения – и соответствующие средства синхронизации. В дополнение к задачам имеются "защищенные" модули (программируемый аналог монитора Хоара). В ООП-языках вводятся отношения наследования (пусть далеко не везде классы являются одновременно модулями, но сейчас важна идея). Вообще же классы должны быть first-class (полноправными) entities. Так, в Зонноне есть традиционные модули, интерфейсы, их реализации («обычные» классы), и еще есть средства задания протоколов взаимодействия модулей (в том числе, и асинхронных взаимодействий). В Аде модулем может выступать как «пакет» или «задача», так и одиночная подпрограмма. В ней же есть separate-подпрограммы (пусть с довольно слабой семантикой и с ограничениями).

Еще раз повторяю, сейчас речь идет не "вообще", а именно об адекватности инструмента большим и сложным задачам. "Вообще" же Оберон мне очень нравится: помимо аккуратного дизайна, он несет в себе какую-то позитивную ауру, что ли... Программируя на нем, чувствуешь себя комфортно и уютно. Чего и близко нет в Си. Но это - личное, прошу прощения. :-)

5 комментариев:

Анонимный комментирует...

> Си: линейная последовательность функций. Си-функция не может хранить собственный контекст между своими вызовами (это «stateless»-компонент), поэтому для обеспечения сколько-нибудь нетривиального взаимодействия приходится активно использовать глобальные переменные.

То есть как так? А как же static-переменные внутри функций? Они же инициализируются один раз при первом вызове функции, а затем сохраняют своё значение между вызовами функций:

Programming languages — C

6.2.4 Storage durations of objects

3 An object whose identifier is declared with external or internal linkage, or with the
storage-class specifier static has static storage duration. Its lifetime is the entire
execution of the program and its stored value is initialized only once, prior to program
startup.

Вот в Обероне такой вещи нет, хотя own-переменные были ещё в Алголах...

> (Авторы Ады ужасно гордились, что они сделали свой язык объектно-ориентированным, добавив в него всего лишь шесть, кажется, новых служебных слов. Нашли чем гордиться: Вирт не добавил вообще ни одного...)

В Смоллтоке всего пять ключевых слов на всё про всё. В Io -- вообще нет ни одного ключевого слова... :о)
_______
Geniepro

Анонимный комментирует...

G> То есть как так? А как же static-переменные внутри функций?

Набор static-переменных для функции существует в единственном числе (тот же синглтон).
А здесь, возможно, имеется в виду некий индивидуальный контекст, что-то вроде замыканий (или объектов).

Анонимный комментирует...

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

С тем, что модульность иногда важнее ООП, согласен.
Но не понимаю, чем Вам раздельная компиляция "не угодила"? :) Возможно, здесь просто терминологическое расхождение (иногда раздельной (separate) компиляцией называют то, что - в терминологии Вирта - называется независимой (independent) компиляцией).

Но вот достаточно ли простого набора одинаковых сущностей (и опять, как и в Си, линейного), чтобы адекватно отражать многообразные отношения между частями создаваемой системы?

Почему это набор модулей линейный, как список функций в Си? Между модулями существуют отношения экспорта/импорта, благодаря которым (1) они образуют иерархическую расширяемую структуру (даг), (2) корректный порядок инициализации модулей гарантирован.

Так, да не совсем: расширяемые записи (аналог классов в более распространенных языках) не являются средством структурирования программы в целом.

Слишком категорично сказано, вызывает сомнение.
Особенно, если вовремя вспомнить об обероновской программной шине или (на тему того, что модули как-то ограничивают записи) инсталлируемых каталогах объектов (например, в BlackBox).

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

А мне думается, что обероновское решение, дублированное в Аде, вводит принципиально другую (новую, если сравнивать с Си/Си++) архитектуру ПО: "тэгированную" (типизированную) память.

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

А что значит программировать объектно? Возьмем в качестве примера (абстрактную) современную графическую ОС, основанную на обмене сообщениями.
Ее объектная ориентированность вроде бы не подлежит сомнению. И на каком языке ее лучше программировать: на Си++ с его first-class классами :) или Обероне с его расширяемыми записями и проверкой динамического типа всего за одно сравнение? Можно даже сказать, что обероновская программная шина представляет собой простейшую реализацию схемы двойной диспетчеризации.

Надеюсь, из сказанного примерно понятно, чего мне недостает в Обероне.

Если я правильно понял, в Обероне Вам в основном недостает (1) более разнообразной (специалированной) модульности и (2) классов. Честно говоря, с первым согласиться легче. :)
Второе, по видимости, движение в том же направлении (по Мейеру, класс = тип + модуль), но вызывает большее сомнение: как уживутся "две женщины на одной кухне" (класс и модуль :) ). Ведь тот же Си++, где классы являются объектами первого рода, - язык определенно не модульный.

Не могли бы Вы подкинуть немного "инсайдерской" информации и ответить на давно мучающий меня вопрос? :)
Почему в Обероне нет шаблонов - понятно. Но почему в нем не прижились даже "облегченные" дженерики, предлагавщиеся в середине 90-х Шиперским?
Я задавал когда-то подобный вопрос Гуткнехту, но, к сожалению, не понял ответа (вероятно, помешал языковой барьер).

_winnie комментирует...

Про то, что в С мы обязаны использовать глобальные переменные - не очень понятно. Обычно функции передаётся некий указатель, и она с ним работает как с this.
Очень многие (вменяемые) сишные библиотеки так сделаны.

pm комментирует...

есть "только один способ борьбы со сложностью: делить сложное целое на части так, чтобы части не вмешивались в дела друг друга"

Не один. Например в геометрии сложную задачу удается сделать простой правильно выбрав систему координат.
В программировании есть аналогичный прием - называется DSL.