Ну вот, собрался, наконец, написать о том, как я преобразовал С++ в domain specific, declarative, functional, aspect-oriented, event-driven language :)
Disclaimer: Описанная здесь идея не имеет никакого отношения к моей работе и к технологии, которая используется в моей работе. Это моё личное "изобретение". Оно совершенно абстрактно и может применяться в любых областях.
Может, это я изобрёл такой велосипед, не знаю. В любом случае:
Возьмём обычную модель обработки непрерывного входного потока (input stream), который поделён на некие куски информации (messages), которые поделены на ещё более мелкие куски (fields), и т.д.
Обычно для каждого сообщения создаётся какой-нибудь класс, содержащий business logic. Этот класс принимает входные поля, объявляет всякие промежуточные переменные, вычисляет эти переменные, используя входные данные, проходит по разным веткам if-then-else и в конце концов вычисляет выходные объекты, которые посылает в какой-нибудь выходной поток (output stream).
Моей главной целью здесь было преобразовать программную модель таким образом, чтобы описывать не алгоритм выполнения задачи, а цели, которые поставлены перед программой (declarative programming). Эти цели будут "нанизаны" на "дерево" контроля, которое будет легко читаемым, и таким образом, должно быть сразу видно, что же делает эта программа.
Я подумал, что здесь можно сделать несколько вещей:
1) Вычленить из всех многочисленных внутренних разветвлений программы одно большое дерево (flow chart), которое бы содержало всю (во всяком случае - основную) логику программы.
2) Каждый узел и лист этого дерева описывался бы неким wrapper-объектом, который был бы связан с функцией-декларацией, вычисляющей этот объект. Такая функция не должна содержать никаких условий - то есть, она не описывает как вычислять значение, она просто говорит, что представляет собой это значение (в идеале такая функция содержит единственный оператор присвоения).
Wrapper-объекты бывают такими:
* объекты ввода
* объекты вывода
* операторы switch (которые распределяют контроль выполнения по разным веткам)
* внутренние переменные
* действия (то есть, объекты, "обёрнутые" вокруг методов).
3) Каждый из wrappers посылал бы events, таким образом сообщая всем заинтересованным сторонам о том, что с ним происходит (см. ниже).
4) Объекты ввода подписываются на events входного потока. Так они узнают о том, что у них появились значения, и всё "дерево вычислений" приходит в движение.
5) Выходной поток подписывается на events объектов вывода. Таким образом он сам распоряжается тем, что, когда и куда выводить. Сама главная программа ничего не знает о том, что происходит с объектами вывода.
Новая диаграмма получилась такой:
Главный "flow chart" создаётся с помощью операторов ввода и вывода, примерно так:
InputStream >> Field1
>> Field2
>> Field3
...
Switch1 >> Action1
>> Action2
>> Action3
>> Switch2;
Switch2 >> Action4
>> Action5;
Action1 >> Output1
>> Output2;
Action2 >> Output3
>> Output4
>> Output5;
OutputStream << Output1
<< Output2
<< Output3
...
Теперь посмотрим, что такое VariableWrapper (он также включает в себя InputWrapper и OutputWrapper):
VariableWrapper содержит внутри себя значение, которое может быть присвоено только один раз (no reassignments!), а также associated processing method, который вызывается только один раз, чтобы присвоить переменной её значение. Этот метод не должен иметь никаких побочных эффектов, обычно он состоит только из оператора =. Естественно, каждая переменная обычно зависит от нескольких других переменных: все их значения будут вычисляться lazily, по необходимости и только один раз.
Типичные методы вычисления переменных могут выглядеть так:
void defineA()
{
A = (B() + C())/2*D();
}
void defineB()
{
B = E()/F();
}
и так далее (содержание функий я сделал весьма нелепым, но это просто чтобы показать принцип).
Все действия с переменными производятся с помощью трёх операторов: = (присвоить), () (получить значение) и bool - узнать, присвоено ли переменной значение.
Если мы вдруг присвоили в функции defineA() значение другой переменной, то при попытке получить значение А, будет брошен "not assigned" exception, потому что значение А ещё не присвоено.
И наоборот, если в функции defineA() мы попытаемся присвоить значение ещё и переменной В, то при повторной попытке присвоить В будет брошен "reassignment" exception.
Всё это даёт нам некое подобие functional programming.
Таким образом, можно представить, что выполнение программы идёт как бы по двум большим ветвям: контроль "спускается" по явно заданному flow chart от верхнего уровня (когда определяется ввод) к нижнему (когда определяется вывод), в то время как данные "текут" по дереву зависимости переменных, неявно определённому самой структурой программы, от вводных полей, до объектов вывода:
Теперь посмотрим на ActionWrapper:
Здесь всё совсем просто, это не более чем завёрнутая в оболочку функция.
Вы, конечно, заметили, что VariableWrapper и ActionWrapper посылают события (events) при каждом удобном случае: Variable Assigned Event, Variable Accessed Event, Before Action Event, After Action Event.
Это даёт нам совершенно удивительные возможности.
Во-первых, Aspect-Oriented programming. Не нужно никаких специальных процессоров и препроцессоров. Если мы, например, хотим писать в logfile перед каждым выполнением (или после) определённой функции - нет ничего проще: достаточно только подписаться на events этой функции (это может сделать какой-нибудь LogWriter) и дело сделано. Заметьте, что основная программа абсолютно ничего не знает ни о каком логфайле или других подписчиках - код остаётся нетронутым. То же самое можно проделать с каждой переменной, вводным параметром или выводом программы, код опять же остаётся нетронутым. Любое "ортогональное" основной программе действие можно производить "за кулисами", не трогая при этом саму программу.
Во-вторых, можно выводить debug information, следить за разными проходами по дереву контроля, записывая таким образом различные test cases, и т.д. Программа может создавать свою собственную документацию прямо во время выполнения, и эта документация всегда будет правильной. Можно даже сделать специальный debugger (что я и сделал), который будет останавливаться на любых заданных breakpoints с любыми условиями (такая-то переменная получила такое-то значение). Достигается это за счёт того, что посылка events - это на самом деле function call, который получает контроль. Мой дебаггер, например, это GUI аппликация, которая работает на PC и контролирует программу, бегущую на Юниксе, за счёт того, что его ассистент на Юниксе получает контроль в event handler и держит его столько, сколько надо пользователю). Кроме того, мой дебаггер умеет рисовать красивое дерево контроля - как уже стало понятно, здесь всё готово для того, чтобы такую диаграмму было легко и удобно создавать, ведь каждый объект знает о своих предках и потомках.
Опять же - сама программа ничего не знает ни о каком дебаггере, и ни одной строчки кода менять в ней не нужно!
Почему я называю всё это DSL (Domain Specific Language), кроме всего прочего ?
Всё на самом деле зависит от того, какой смысл придать Wrapper Objects. Назовите их так, чтобы они наиболее близко отражали ту область, с которой работает ваша среда - и можете программировать уже не на С++, а на своём собственном DSL. Ведь этот дебаггер, который я описал выше - это на самом деле семантический дебаггер: он работает не на уровне переменных и функций С++, а на уровне переменных (объектов) и действий той области, в которой его применяют. Он понимает их смысл.
Всё!
no subject
Date: 2010-07-16 10:05 pm (UTC)По сей день существует одна программа, которая этим пользуется, и я всё думаю, как бы найти время её переделать, потому что поддерживать её невозможно.
С тех пор я решил больше страшными темплейтами не пользоваться. В этой библиотеке они всё равно есть, конечно, но всё очень просто и по делу.