среда, 31 октября 2007 г.

Простота и сложность: завершение

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

Конечно, отвечать на комментарии было бы логично в соответствующем разделе. Но развернутый ответ там не напишешь: получается слишком длинно и «некрасиво», потому приходится делать отдельный пост.

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

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

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

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

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

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

Комментарии AVP и мои комментарии на комментарии :-)

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

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

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

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

Ну, примерно так. Мне бы хотелось, чтобы в языке были развитые средства структурирования программ (та самая модульность), а уж техническая возможность компилировать структурные компоненты программы по отдельности подразумевалась бы как естественное следствие этой модульности. Собственно, примерно как в Аде. Модуль (пакет, подпрограмма, задача) – это языковое понятие; раздельная компиляция – больше требование к среде программирования: - к компилятору, линкеру и т.д.

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

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

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

Конечно, линейный. Отношения импорта/экспорта сами по себе не дают нового качества. Функции в Си характеризуются такими же по сути отношениями импорта/экспорта: каждая функция экспортирует свое имя и импортирует имена всех доступных функций и глобалов. Оберон-модули как таковые не образуют расширяемую иерархическую структуру: такую структуру образуют расширяемые типы внутри модулей. Структура Оберон-программы, скорее, маскирует, скрывает расширяемую иерархию типов. Иными словами, сейчас Оберон-модули – это однородный набор контейнеров, по которым распределено дерево типов.

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

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

Слишком категорично сказано, вызывает сомнение.

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

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

Я, честно говоря, не очень осведомлен насчет решений, воплощенных в Блэкбоксе, но подозреваю, что каталоги объектов – это, судя по названию, средство, помогающее программисту увидеть, какие же типы содержатся в модулях программы и каковы отношения между этими типами. Иными словами, если я прав, то Блэкбокс как раз пытается преодолеть тот недостаток Оберона, о котором я и говорю.

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

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

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

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

Честно говоря, я бы уклонился от общей дискуссии об ООП; наверное, я не чувствую себя для этого достаточно подготовленным (это все-таки не совсем компиляторная тема :-)). Пишу «объектно» вполне уверенно, а вот рассуждать и дискутировать... Извините.

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

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

Если я правильно понял, в Обероне Вам в основном недостает (1) более разнообразной (специалированной) модульности и (2) классов.

Примерно так. Только я не стал бы разделять эти два пункта. Мне представляется, что «класс» в том виде, как он есть в том же Си++, должен иметь статус полноправного (специализированного) модуля, наряду с другими.

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

Что значит «уживутся»? Они друг другу никак не мешают, скорее необорот, помогают и дополняют, и вместо Вашего образа двух женщин на одной кухне я бы предложил, скажем, двух рабочих, несущих что-то тяжелое. Любой из них в одиночку груз даже не поднимет, а вместе они его не просто подняли, но даже и перенесли и поставили куда надо...

Ведь тот же Си++, где классы являются объектами первого рода, - язык определенно не модульный.

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

Не могли бы Вы подкинуть немного "инсайдерской" информации и ответить на давно мучающий меня вопрос? :)

Почему в Обероне нет шаблонов - понятно. Но почему в нем не прижились даже "облегченные" дженерики, предлагавщиеся в середине 90-х Шиперским?

Я задавал когда-то подобный вопрос Гуткнехту, но, к сожалению, не понял ответа (вероятно, помешал языковой барьер).

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

Я несколько раз говорил с Виртом насчет generic programming (пытался рассказать ему, о чем, собственно, читаю лекции в ETH :-)), но особого интереса не почувствовал. Он когда-то общался с Александром Степановым, и мне показалось, что и тот не смог объяснить ему, чем важны и хороши дженерики... Кроме того, у Вирта, если я правильно ощутил, есть какое-то неприятие Степанова лично. Впрочем, не уверен, могу ошибаться. Возможно, он просто не считает GP столь уж важным подходом и потому относится не слишком серьезно и к его создателю.

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

Сергей Губанов комментирует...

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

Почитайте диссертацию Regis Crelier (ETH Th10650) Separate Compilation and Module Extension, и, всё таки, ознакомьтесь с BlackBox, будет весьма пользительно.

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

Уважаемый Сергей Губанов. Надеюсь, что «ноги вашего непонимания» моего текста происходят всего лишь из его (текста) невнимательного прочтения, а также из горячности, обычно свойственной неофитам. Последнее тем более странно, что Вы, несомненно, являетесь весьма начитанным специалистом, особенно сильным в трактовке определений фундаментальных понятий и непримиримостью к тем, кто эти определения безответственно искажает. Тем не менее, я был бы Вам крайне признателен, если бы Вы сделали тональность Ваших комментариев менее назидательной.

Сергей Губанов комментирует...

>> ...если бы Вы сделали тональность Ваших комментариев менее назидательной...

Да я действительно допустил типичную ошибку свойственную неофитам - выдал некую риторику в назидательном тоне не имея при этом власти заставить в неё поверить, прошу меня за это простить.

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

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

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

Я, например, могу сказать своим студентам, что тип (класс) в некотором смысле является модулем. Но только в некотором смысле. Во всех смыслах я бы говорить не рискнул.

Еще раз прошу прощения, что вмешиваюсь.

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

Спасибо за ответы.

Согласен, что иногда лучше прервать/приостановить выдыхающуюся дискуссию, чтобы освободить время для новых дел. :)

Постараюсь кратко высказать, что осталось для меня неясным.

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

Я не понял также, почему на Обероне "неудобно программировать объектно". (Насчет "адского" :) ООП ничего не могу сказать -- нет соответствующего опыта.)
Если Вы имели в виду отсутствие в Обероне-1 присоединенных к типу процедурных констант (методов класса), то они есть и в Обероне-2, и в Компонентном Паскале. Решает ли это проблему?
Пока не вижу, что принципиально необходимого для ООП может добавить выделение класса в отдельную языковую конструкцию.

С уважением,
AVC

Сергей Губанов комментирует...

...что считается модулем...

Там хорошо написано:
http://wiki.oberoncore.ru/index.php/%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8C

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

Виноват, совсем забыл про локальные статики. Каюсь.

Зря извинялся.
Если к переменной нужен доступ не одной функции, а - как в твоем примере "влияния на мышление" - группы. статики не помогут.