четверг, 20 сентября 2007 г.

Синтаксис и семантика, простота и сложность

Этот пост - нечто вроде ответа на комментарии (см. "О сайте compilerjobs.com") насчет синтаксической сложности C++ и трудностях его освоения.

Очевидно, при разработке C++ перед Страусом стояли два неотменимых предусловия. Во-первых, обеспечить совместимость с Си и, в частности, добиться стилистической преемственности с языком-предшественником (впрочем, я вполне допускаю, что эта самая стилистическая преемственность подразумевалась по умолчанию - складывается впечатление, что эта братия из AT&T просто не могла представить, что можно придумать что-то получше, чем их птичий язык...) И во-вторых, отразить в языке как можно больше потребностей программистов и - что даже важнее - постараться предупредить все возможные ситуации, когда то, что они придумали, окажется в противоречии с обстоятельствами реального программирования. В этом смысле очень интересно сравнить первоначальные, абсолютно умозрительные наброски "шаблонных" конструкций из "Зеленой книги" (Язык Си++ с комментариями, "Мир", 1991), когда никто, включая Страуса, даже не пробовал их использовать, и окончательный вид главы 14 Стандарта... После того, как было решено включить в Стандарт STL Степанова, в шаблоны было сделано громадное количество усовершенствований и поправок - от принципиальных до самых что ни на есть мелких, которые вот именно пытались учесть и предвосхитить все возможные будущие проблемы. Еще и не все внесли, можно было кое-что добавить...

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

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

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

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

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

Так что ни один из этих подходов я в полной мере не разделяю. И если когда-нибудь наступит период хотя бы относительного финансового благополучия, и надо мной не будет висеть срочная работа, и другие обстоятельства сложатся благоприятным образом (я капризен, к сожалению и стыду...), то я обязательно попытаюсь описать и, возможно, реализовать язык, который кажется мне оптимальным компромиссом между чрезмерной сложностью C++ и чрезмерной же простотой Оберона. И, конечно, мой язык будет гораздо элегантнее обоих :-). Я даже название ему придумал: Zeppelin. И логотип: контур двухгрифовой гитары Джимми Пейджа в овале, повторяющем форму дирижабля. (много смайликов)

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

И благодарите Бога, что мы говорим сейчас о "нормальных" языках: попытайтесь хотя бы отделить синтаксис от семантики, скажем, в языке Haskell...

Вдогонку

К вопросу об "элегантности" C++ и о стиле программирования, им навязываемом.
Вспомнил, как несколько лет назад (в 2002 году, кажется) в ETH случился "симпозиум по языкам программирования". Под этим громким названием скрывались всего три доклада, однако они и их авторы стоили иной многодневной конференции: выступали Страуструп, Дал (автор Симулы) и Вирт.

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

(Уж не говоря о том, что примеры эти, как и в его книге, были набраны пропорциональным шрифтом и вдобавок курсивом. Как хотите, но в этом одном мне видится явное извращение...)

Кто-то из озадаченно молчащего зала растерянно заметил что-то насчет крайней неудобочитаемости (в подтексте звучало: нелепости) примеров. Страус отреагировал: ну, это не проблема, программисты к такому стилю быстро привыкают.

Ну что тут скажешь...

23 комментария:

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

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

Короче говоря, см. SymADE (http://www.symade.org / .com), Intentional Programming и иже с ними.

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

Ничего сложного в отделении синтаксиса от семантики для того же Haskell нет - см. Template Haskell.

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

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

Интересно, почему модульности очевидно недостаточно для борьбы со сложностью?
И если модульности недостаточно, то какое языковое свойство справится с этой задачей лучше?

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

А как же ООП, имеющийся в Оберонах?
Вот дженериков как в Аде или (хотя бы) шаблонов как в С++ или (гораздо лучше) полиморфных типов и функций как в Хаскелле не хватает в Оберонах, на что оберонщики придумывают какое-то собственное метапрограммирование на базе RTTI оберон-среды...

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

Эх, старый добрый С++ понемногу уходит куда-то в сторону c#. Появляется чувство, что простой системный код стоит снова писать на C и ASM. Нет, дело не в сложности языка. Дело в том, что современными компиляторами уже мало представляешь, какой код получится после компиляции.
Присоединюсь к mkz - отображение кода в виде дерева было бы невероятно удобным. До сих пор с завистью вспоминаю маткад с его строгим отображением формул-процедур :)

Yuri Volkov комментирует...

извините, что пишу этот комментарий сюда, но не могли бы вы включить в настройках full RSS feed? А то в фидридере ваши посты не полностью :-(. Cпасибо.

Alexey Zakhlestin комментирует...

шаблоны можно было сделать элегантнее!
см. http://www.digitalmars.com/d/template-comparison.html

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

To Yuriy Volkov: я поставил галочку в соотв. месте. Надеюсь, теперь все будет нормально. - EZ

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

To CTpaHHoe:

>Появляется чувство, что простой системный код стоит
>снова писать на C и ASM.

Да, конечно - если речь идет именно о _системном_, то есть специфическом, низкоуровневом коде, который имеет особую природу и особые требования (по памяти/быстродействию),- такой код очень часто имеет явный смысл писать на С.

>Присоединюсь к mkz - отображение кода
> в виде дерева было бы невероятно удобным.

Я готов с этим согласиться - более того, в последнее время пытаюсь сделать кое-что в этом направлении для Си++; через некоторое время собираюсь выложить кое-какие свои соображения на этот счет.

Но... дьявол, как обычно, в деталях. Надо очень много думать о том, что и как именно отображать в виде дерева.

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

И последнее: насколько я понял, идея mkz не только и не столько в том, чтобы предоставить дерево для программиста, а скорее в создании некоего универсального древовидного представления, на основе которого можно было бы создавать собственные специализированные расширения языка. Это идея концептуально очень мощная, но все-таки не носит характера конкретного сервиса для программистов.

А вообще, я собираюсь об этом вскоре написать отдельный пост - меня есть кое-какие свои идеи по этому поводу.

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

To AVC:

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

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

>И если модульности недостаточно, то какое
> языковое свойство справится с этой задачей лучше?

Еще раз простите, но тут что-то не в порядке с логикой. Да, модульности недостаточно, но это вовсе не означает, что модульность надо заменить на что-то, что "справится с задачей лучше".

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

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

- типовАя параметризация
- полноценная поддержка ООП, включая интерфейсы/контракты
- механизм исключительных ситуаций, интегрированный с ООП

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

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

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

-ЕЗ

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

>при разработке C++ перед Страусом стояли

"Страус" = это оговорка ?

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

>"Страус" = это оговорка ?

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

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

> Мне казалось, это вполне в ихнем (т.е. в американском) стиле - сокращать длинные и не слишком гладко читаемые фамилии.

Как раз наоборот, в Америке к личным именам относятся очень аккуратно.

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

To Pavel:
Ну согрешил, прошу прощения. Я все-таки сталкивался с подобными сокращениями, правда, в устной речи и не часто. Нашего Солженицына они "Солжем" звали, так мне показалось, что и Страуструпа можно аналогично сократить... :-)

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

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

Ну так сравните сложность C++ и сложность Ады.
В C++ мы должны не забывать ставить проверки.
В Аде мы проверки опционально снимаем.
В C++, чтобы сделать объект некопируемым, мы переопределяем operator= и конструктор копирования в приватной части.
В Аде мы пишем limited.
В C++, чтобы определить метод как виртуальный, мы пишем =0 после прототипа.
В Аде мы пишем is abstract.
В C++ тела настраиваемых методов должны быть в ашниках, а ненастраиваемых — в цппшниках.
В Аде все (видимые снаружи, я имею в виду) определения в .ads, все тела в .adb.
В C++ нельзя подключить пространство имён в ашнике. (точнее, можно, но по головке за такое не погладят)
В Аде ... ну, ясен пень, такая тупость только в C++ может быть, в Аде всё в порядке.
В C++ простые аргументы нужно передавать по значению, а большие — по ссылке, иначе они полностью копируются.
В Аде входные параметры (с модификатором in по умолчанию) доступны только для чтения, поэтому копировать их не нужно. А уж как их передать на машинном уровне — это дело компилятора.
В C++, чтобы настроить шаблон дробным числом, необходимо настроить его ссылкой на это дробное число.
В Аде ... ну, ясен пень, такая тупость только в C++ может быть, в Аде всё в порядке.
В C++ namespace может быть одним, имя цппшника/ашника другим, а объектный файл, который нужно не забыть подключить — третьим.
В Аде между всеми тремя несложное соответствие. Да и то, реально знать нужно только namespace.

Это лишь несколько примеров к слову об адекватности сложности C++, которой здесь было посвящено столько воды.

А про Оберон (про первый) ещё Жан Ичбиа, создатель Ады говорил :
"There are times when Wirth believes in small solutions for big problems. I don't believe in that sort of miracle. Big problems need big solutions!"

Правда, под big solutions подразумевались именно big solutions, а на big mess.

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

идея mkz ... скорее в создании некоего универсального древовидного представления, на основе которого можно было бы создавать собственные специализированные расширения языка. Это идея концептуально очень мощная

А почему дерево, а не граф?

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

To mkz: Конечно, граф. Просто все писалось в контексте, в котором употреблялось "дерево" (и на Вашем сайте, вроде бы, тоже), ну, я и продолжил.

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

(Это можно красиво на картинке показать: есть дерево программы, и по нему - другим цветом - проложены семантические связи между узлами. Скоро нарисую. :-))

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

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

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

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

А что Си++?
Вот старая шутка:
#define private public

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

есть дерево программы, и по нему - другим цветом - проложены семантические связи между узлами.

То есть программа дерево обязательно? Это от ООП, практика парсинга в компиляторах или вообще закон природы?

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

есть дерево программы, и по нему - другим цветом - проложены семантические связи между узлами.

То есть программа дерево обязательно? Это от ООП, практика парсинга в компиляторах или вообще закон природы?


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

Еще раз подчеркиваю, что речь идет о структуре программы - более сложные отношения в дерево, очевидно, не втиснешь.

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

Вот несколько моих постов в форумах, которые могут быть полезны для Вашей разработки «древовидного» Си++:


Набросок иконок для древовидного программного модуля пост-Оберона:
http://skytaxi.narod.ru/Icons.mht


http://gurin.tomsknet.ru/visual2k.html (вставка языковых конструкций щелчком мыши по легкодоступной кнопке (в верхней "панели операторов"), наглядные пиктограммы языковых конструкций, структуризация программного кода с помощью дерева с "плюсами" и "минусами", легкость задания кусочно-линейной функции, операторы "Параллельный процесс", «Пауза», «Ожидание события», «Посылка сообщения», «Ожидание сообщения», «Прекращение параллельного процесса»; а вот задание параметров в отдельной панели - не очень удачный подход (он хорош только для форм), и лучше бы транспонированная табличка с параметрами была интегрирована прямо в код)
См. раздел "Пример реализации" в http://www.delphikingdom.ru/asp/viewitem.asp?catalogid=454

Вот еще один пример представления программного модуля в виде дерева:
http://www.wentor.ru/img/products/ring.gif


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

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

http://rsdn.ru/article/philosophy/LOP.xml (цитата: "Мы должны хранить программы прямо в виде структурированного графа")

http://www.eplsw.com/Introduction_IDE.asp (внедрение таблиц с параметрами непосредственно в программный код, показ передачи управления стрелками слева от программного кода, синхронное переименование переменных)


http://community.sharpdevelop.net/blogs/mattward/articles/FeatureTourCodeGeneration.aspx (выбор вставляемого шаблона кода из списка, раскрывающегося непосредственно в точке вставки; применение шаблона составного оператора к выделенной области кода)

Вот ссылка на более-менее систематизированное описание того, как графический интерфейс программирования должен выглядеть по моему мнению (это лишь первый набросок):
http://forum.oberoncore.ru/viewtopic.php?f=7&t=389&st=0&sk=t&sd=a&start=20#p7339

Сергей Прохоренко
sergeyprokhorenko [at] yahoo.com.au

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

Чувак переходи на Action Script

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

разработка разработка сайтов http://web-miheeff.ru разработка