Показаны сообщения с ярлыком программирование. Показать все сообщения
Показаны сообщения с ярлыком программирование. Показать все сообщения

суббота, 21 июня 2014 г.

Шаблонное метапрограммирование в C++: генерация списков с произвольными полями

Я приведу псевдокод, который отразит смысл задачи.
list  Person1;

Person1.add_record( Name: "Paul" );
Person1.add_record( Age: 45 );
Список Person1 из этого примера можно реализовать с помощью структуры с полями Name и Age. Но в следующем определении мы, возможно, захотим изменить поля списка и нам понадобится новая структура. Как обойтись без явного определения структур-контейнеров и использовать одно обобщенное определение? Слово обобщенный (generic) содержит ответ — нужно привлечь к решению этой задачи шаблоны C++. Нам нужны линейные комбинации полей списков. Публичное наследование в C++ — отличный инструмент для создания линейных комбинаций! Я приведу код, добавляющий новую запись в список, а затем объясню его смысл. Имя файла — generator.hh. Здесь и далее стражи включения пропущены ради лаконичности.
#define DECLARE_RECORD( Name, Type, Value ) \
    template < typename  Parent >           \
    struct  Rec##Name : public Parent       \
    {                                       \
        Rec##Name() : Name( Value ) {}      \
        Type  Name;                         \
    };                                      \
    template <>                             \
    struct  Rec##Name< Generator::End >     \
    {                                       \
        Rec##Name() : Name( Value ) {}      \
        Type  Name;                         \
    };

namespace  Generator
{
    typedef int  End;
}
Макрос DECLARE_RECORD объявляет шаблоны структур с именем Rec##Name, которые содержат одну единственную запись, соответствующую одной из записей в формируемом списке: он реализует функцию add_record() из приведенного выше псевдокода. Структура может наследовать типу Parent — такой же структуре с другой единственной записью, либо замыкаться с помощью объявления Generator::End. Тип этого объявления может быть достаточно произвольным, я выбрал int. Формирование списка должно представлять собой создание списка типов из типов, объявленных с помощью DECLARE_RECORD. Давайте создадим простую базу персональных данных: файл persons.hh.
#include "generator.hh"

#define DECLARE_PERSON( Name, Id ) \
    Persons::Id::RecName< Persons::Id::RecAge< Generator::End > >  Name;

namespace  Persons
{
    namespace  Person1
    {
        DECLARE_RECORD( Name, const char *, "Paul" )
        DECLARE_RECORD( Age, size_t, 45 )
    }

    namespace  Person2
    {
        DECLARE_RECORD( Name, const char *, "Helen" )
        DECLARE_RECORD( Age, size_t, 38 )
    }
}
Макрос DECLARE_PERSON — это и есть механизм для формирования списка с записями имени (Name) и возраста (Age) персоналий. Список формируется нанизыванием имен структур, объявленных макросами DECLARE_RECORD ниже и заканчивается объявлением Generator::End. Для различения персоналий используются пространства имен Person1 и Person2. Заметьте, что здесь нет ни объектов времени исполнения, ни объектов времени компиляции: объявления DECLARE_RECORD разворачивают объявления шаблонов на стадии препроцессорной обработки, инстанцирование же этих шаблонов будет происходить во время применения макроса DECLARE_PERSON. Давайте напишем простой тест (файл test.cc).
#include <iostream>
#include "persons.hh"

int  main( void )
{
    DECLARE_PERSON( rec1, Person1 )
    std::cout << "1. Name: " << rec1.Name << ", Age: " << rec1.Age << std::endl;

    DECLARE_PERSON( rec2, Person2 )
    std::cout << "2. Name: " << rec2.Name << ", Age: " << rec2.Age << std::endl;
}
После компиляции запустим программу на исполнение.
g++ -o test test.cc
./test
1. Name: Paul, Age: 45
2. Name: Helen, Age: 38
Работает. А теперь создадим базу данных книг в файле books.hh. В каждой записи будет три поля: название (Name), автор (Author) и год выпуска (Year).
#include "generator.hh"

#define DECLARE_BOOK( Name, Id ) \
    Books::Id::RecName< Books::Id::RecAuthor< \
                                Books::Id::RecYear< Generator::End > > >  Name;

namespace  Books
{
    namespace  Book1
    {
        DECLARE_RECORD( Name, const char *, "War and Peace" )
        DECLARE_RECORD( Author, const char *, "Lev Tolstoy" )
        DECLARE_RECORD( Year, int, 1873 )
    }
}
Макрос DECLARE_BOOK выполняет ту же роль, что и DECLARE_PERSON при создании списка персональных данных, то есть создает список данных одной книги путем нанизывания типов, объявленных ниже с помощью макросов DECLARE_RECORD, в список типов, замыкаемый объявлением Generator::End. Добавим нашу новую книгу в тестовую программу. Для этого в файле test.cc нужно включить файл books.hh
#include "books.hh"
и в конце функции main() поместить строки
    DECLARE_BOOK( rec3, Book1 )
    std::cout << "3. Name: " << rec3.Name << ", Author: " << rec3.Author <<
                 ", Year: " << rec3.Year << std::endl;
Компилируем и запускаем на исполнение.
g++ -o test test.cc
./test
1. Name: Paul, Age: 45
2. Name: Helen, Age: 38
3. Name: War and Peace, Author: Lev Tolstoy, Year: 1873

воскресенье, 15 мая 2011 г.

Сравнение подходов в реализации алгоритмов на C++ и Haskell

Классический пример краткой и ясной реализации алгоритма на Haskell - это быстрая сортировка:
quickSort [] = []
quickSort (x:xs) = quickSort [ y | y <- xs, y < x ] ++ [ x ] ++
                   quickSort [ y | y <- xs, y >= x ]
Обычно рядом с этим кодом приводят какую-нибудь 20-строчную реализацию быстрой сортировки на C или C++. И конечно же сравнение идёт не в пользу C++. Откровенно говоря, здесь заключена немалая доля лукавства: как можно сравнивать высокоуровневые определители списков (list comprehensions - выражения в квадратных скобках в данном примере) с низкоуровневыми итерациями в коде C++? С другой стороны, определители списков являются встроенным механизмом Haskell, поэтому подобное сравнение всё-таки допустимо.

Ниже приводятся реализации одного алгоритма на C++ и Haskell. Алгоритм был взят из книги Ф. Меньшикова "Олимпиадные задачи по программированию". Задача формулируется следующим образом:
Дана последовательность из N целых чисел чисел. Необходимо удалить из последовательности минимальное количество чисел так, чтобы оставшаяся часть последовательности оказалась строго возрастающей. Иными словами нужно найти самую длинную возрастающую подпоследовательность.
Базовый алгоритм для решения этой задачи приводится в этой же книге:
Начиная с первого элемента последовательности и заканчивая последним найти максимальную длину возрастающей подпоследовательности из предшествующих элементов, меньших данного, и прибавить к ней 1. Эта величина будет соответствовать максимальной длине возрастающей подпоследовательности, которую можно построить начиная с 1-ого элемента исходной последовательность вплоть до данного элемента. Так, для 1-ого элемента последовательности из предшествующих элементов не существует, поэтому ему соответствует длина 1. Для произвольного i-ого элемента нужно совершить обратный проход к началу последовательности, найти меньший по значению элемент с максимальным значением длины и прибавить к нему 1. После того как длины найдены (при условии, что они сохранены в некотором вспомогательном массиве), нужно в исходной последовательности найти элемент, которому соответствует  максимальное значение длины и, двигаясь к началу последовательности, находить первые элементы, для которых соответствующая длина уменьшилась на единицу. Эти элементы (вместе с исходным) и будут составлять искомую подпоследовательность наибольшей длины.
Несколько замечаний. Во-первых, существуют модификации этого алгоритма, в которых финальный проход от элемента, которому соответствует максимальная длина возрастающей подпоследовательности к началу исходной последовательности не требуется. Поскольку моей целью является не написание самого оптимального алгоритма, а качественное сравнение подобных реализаций на C++ и Haskell, то я использовал базовый алгоритм. Во-вторых, очевидно, что в базовом варианте класс этого алгоритма соответствует O(n^2), так как присутствуют вложенные итерации (обратные проходы к началу последовательности) внутри базовой итерации по всем элементам. В-третьих, могут существовать несколько подпоследовательностей максимальной длины, нам нужно выбрать любую из них.

Итак, реализация этого алгоритма на C++ выглядит следующим образом:
typedef std::vector< int >  VIn;

typedef VIn::value_type     VInElem;

typedef std::vector< int >  VCnt;

void  findLongestIncSeq( const VIn &  vIn, VIn &  vRes )
{
    VCnt                 vCnt( vIn.size() );
    VCnt::iterator       vCntIt( vCnt.begin() );
    VIn::const_iterator  vMaxIt( vIn.begin() );
    VCnt::iterator       vCntMaxIt( vCnt.begin() );
    int                  maxLen( 0 );

    for ( VIn::const_iterator  k( vIn.begin() ); k != vIn.end(); ++k, ++vCntIt )
    {
        int                           maxVal( 0 );
        VCnt::const_reverse_iterator  vCntBackIt( vCntIt );

        for ( VIn::const_reverse_iterator  l( k ); l != vIn.rend();
                                                            ++l, ++vCntBackIt )
        {
            if ( *k <= *l )
                continue;

            if ( *vCntBackIt > maxVal )
                maxVal = *vCntBackIt;
        }

        *vCntIt = maxVal + 1;
        if ( *vCntIt > maxLen )
        {
            maxLen = *vCntIt;
            vMaxIt = k;
            vCntMaxIt = vCntIt;
        }
    }

    std::cout << "Lengths:  ";
    printVec( vCnt );

    if ( vMaxIt == vIn.begin() )
        return;

    std::stack< VInElem >   reverseSeq;
    VCnt::reverse_iterator  vCntBackIt( vCntMaxIt + 1 );

    for ( VIn::const_reverse_iterator  k( vMaxIt + 1 ); k != vIn.rend();
                                                            ++k, ++vCntBackIt )
    {
        if ( *vCntBackIt == maxLen )
        {
            reverseSeq.push( *k );
            --maxLen;
        }
    }

    while ( ! reverseSeq.empty() )
    {
        vRes.push_back( reverseSeq.top() );
        reverseSeq.pop();
    }
}
Вспомогательная функция printVec() реализуется так:
template  < typename  VElem >
struct  PrintVec
{
    void  operator()( VElem  value )
    {
        std::cout << value << " ";
    }
};

template  < typename  Vec >
void  printVec( Vec  v )
{
    std::for_each( v.begin(), v.end(), PrintVec< typename Vec::value_type >() );
    std::cout << std::endl;
}
В функцию findLongestIncSeq() передаются ссылки на исходную последовательность vIn и последовательность vRes, которую нужно заполнить. Внутри функции создаем вспомогательный массив целых чисел vCnt, в котором будем хранить длины максимальной возрастающей последовательности до i-ого элемента включительно. Внутри первого цикла for происходит основная итерация, в которой заполняется массив vCnt, кроме того попутно вычисляются максимальная длина возрастающей подпоследовательности maxLen и соответствующие ей элементы в vIn и vCnt: vMaxIt и vCntMaxIt - они будут нужны для определения элемента, с которого нужно начать финальный проход по исходной последовательности вниз к первому элементу. Этот финальный проход осуществляется в последнем цикле for функции findLongestIncSeq(), в котором заполняется вспомогательный стек reverseSeq. Теперь, чтобы заполнить искомую последовательность vRes, нужно опустошить reverseSeq и сложить его элементы в vRes, что и происходит в последнем цикле while.

Я не буду приводить функцию main() - это детали. Укажу только, что для тестовой последовательности [67 5 34 89 65 12 90 75 8 9 3] выводится следующий результат:
Original: 67 5 34 89 65 12 90 75 8 9 3 
Lengths:  1 1 2 3 3 2 4 4 2 3 1 
Sequence: 5 34 65 90
В строке Original выводится исходная последовательность, в строке Lengths - вспомогательная последовательность длин возрастающих подпоследовательностей, а в строке Sequence - результирующая возрастающая подпоследовательность максимальной длины.

А теперь внимание. Подобный алгоритм написанный на Haskell (я привожу весь код вместе с тестовой частью, так что его можно сразу скомпилировать):
{-find longest increasing subsequence-}

getPrevLength [] = 0
getPrevLength x  = maximum $ doGetLengths x

doGetLengths []     = []
doGetLengths (x:xs) = getPrevLength [ y | y <- xs, y < x ] + 1 : doGetLengths xs

getLengths = reverse . doGetLengths . reverse

doCutTail [] _                                          = []
doCutTail a@(x:xs) maxLen
    | getPrevLength [ y | y <- xs, y < x ] + 1 < maxLen = doCutTail xs maxLen
    | otherwise                                         = a

cutTail [] = []
cutTail x  = reverse $ doCutTail ( reverse x ) ( maximum $ getLengths x )

doFindLongestIncSec' [] _   = []
doFindLongestIncSec' (x:xs) maxLen
    | maxLen - prevLen == 1 = x : doFindLongestIncSec' xs prevLen
    | otherwise             = doFindLongestIncSec' xs maxLen
        where prevLen = getPrevLength [ y | y <- xs, y < x ] + 1

doFindLongestIncSec [] = []
doFindLongestIncSec x  =
    doFindLongestIncSec' ( reverse x ) ( maximum ( getLengths x ) + 1 )

findLongestIncSec = reverse . doFindLongestIncSec . cutTail

{-do some testing-}

doTest x = do
    putStrLn $ "Original: " ++ show x
    putStrLn $ "Lengths:  " ++ ( show $ getLengths x )
    putStrLn $ "Trimmed:  " ++ ( show $ cutTail x )
    putStrLn $ "Sequence: " ++ ( show $ findLongestIncSec x )

main = mapM_ doTest [ [ 10, 5, 89, 16, 78, 67, 56, 34, 12, 10 ],
                      [ 67, 5, 34, 89, 65, 12, 90, 75, 8, 9, 3 ] ]
Я назвал этот алгоритм подобным, поскольку его невозможно реализовать тем же самым способом, что и в C++ (во всяком случае без использования монад). В Haskell отсутствуют итерации и состояния (то бишь переменные), и это означает, что создать вспомогательный массив длин подпоследовательностей и использовать его на том же уровне программного потока не удастся. По той же причине невозможно сначала запомнить индекс элемента с максимальным значением возрастающей подпоследовательности, а затем использовать его по своему усмотрению в параллельной последовательности вызовов функций. В Haskell возможность параллельной последовательности вызовов функций появляется только с использованием монад (см. реализацию doTest выше), а в общем случае вызовы функций могут быть только вложены друг в друга. Это означает, что обычно решение любой задачи в Haskell представляет собой вызов единственной функции, которая в свою очередь вызывает единственную функцию и т.д. Это создает впечатление решения одним махом. В самом деле, в приведенном коде всё решение заключается в вызове единственной функции findLongestIncSeq x. Параллельные вызовы getLengths и cutTail, организованные в doTest нужны только для демонстрации промежуточных состояний исходной последовательности и никоим образом не влияют на результат финального вызова findLongestIncSeq.

Приведенные выше особенности программирования на Haskell требуют более частого использования рекурсивных вызовов, чем в C++, где рекурсия чаще всего заменяется итерацией. Так, функция getPrevLength, которая используется в программе для поиска длины максимальной подпоследовательности, является косвенно рекурсивной благодаря вызову doGetLengths, которая в свою очередь является рекурсивной и косвенно, и явно. Функция cutTail возвращает подпоследовательность исходной последовательности от начала до последнего элемента, которому соответствует максимальная длина возрастающей подпоследовательности. Основная функция findLongestIncSeq является композицией функций reverse, doFindLongestIncSeq и cutTail - это отражено в ее определении.

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

Еще одно важное замечание. Что если мы захотим изменить тип элементов последовательности? Например, захотим искать максимальную подпоследовательность вещественных чисел или строк. В коде на C++ такой вариант предусмотрен с помощью объявления типов VIn и VInElem - стоит поменять тип элемента вектора и всё должно заработать. В коде на Haskell вообще ничего менять не нужно! И это не потому, что в Haskell нет статической проверки типов данных, она там есть, причем более сильная, чем в C++. Просто в данном случае работает параметрический полиморфизм, реализованный в Haskell.
Осталось привести вывод программы на Haskell:
Original: [10,5,89,16,78,67,56,34,12,10]
Lengths:  [1,1,2,2,3,3,3,3,2,2]
Trimmed:  [10,5,89,16,78,67,56,34]
Sequence: [5,16,34]
Original: [67,5,34,89,65,12,90,75,8,9,3]
Lengths:  [1,1,2,3,3,2,4,4,2,3,1]
Trimmed:  [67,5,34,89,65,12,90,75]
Sequence: [5,34,65,75]
Видно, что последний элемент одной и той же тестовой последовательности отличается от результата программы на C++. Это связано с тем, что в C++ варианте последний проход начинался от первого элемента с максимальным значением возрастающей подпоследовательности, а в Haskell варианте - с последнего. Как я уже говорил, могут существовать несколько возрастающих подпоследовательностей максимальной длины, поэтому оба случая являются правильным решением задачи.

суббота, 12 марта 2011 г.

Завершение всех процессов в подгруппе (на примере Tcl)

Предыстория вопроса такова: решил я привести в порядок старый код, написанный в начале 2000-ых  и реализующий набор программ для физического моделирования экспериментальной установки. Ядро программы было написано на фортране с использованием библиотеки Geant3. В результате компиляции и линковки создавались две версии программы - интерактивная и пакетная (batch).

Для удобства использования программ были написаны интерфейсы на Tcl/Tk: большой интерфейс для работы с конфигурацией, графикой, с возможностью запуска обеих версий программ, и малый, который позволял запускать программу в пакетном режиме, следить за ее статусом с помощью прогрессбара (нужно отметить, что программы моделирования и на современных процессорах могут работать в течении суток, а то и недель), и останавливать ее работу при двойном нажатии на специальную кнопку Kill.

Внутри большого интерфейса интерактивная программа моделирования запускалась с помощью открытия канала на запись tcl-командой open, соответственно управление и выход из нее не составляли труда, например для прерывания выполнения программы достаточно было записать в канал, созданный open, последовательность Ctrl-C, а для выхода - последовательность quit. Малый интерфейс мог запускаться самостоятельно, или из большого интерфейса как фоновый процесс.

Поскольку пакетная программа не имела средств для взаимодействия с пользователем, то малый интерфейс запускал ее не с помощью open, а как активный (foreground) процесс. Архитектура запуска процессов малого интерфейса была такова:
  1. В начале работы интерфейса (практически одновременно):
    • Процесс диалога с прогрессбаром и кнопкой Kill (background). В этом диалоге:
      • Отдельный процесс, считывающий результаты программы моделирования, сохраняемые в файле на диске, интерпретирующий их и посылающий с помощью tcl-команды send команды родительскому процессу для отображения статуса выполнения на прогрессбаре (background). В случае прочтения специальной метки end, сигнализирующей о завершении пакетной программы моделирования, данный процесс посылает с помощью send своему родительскому процессу команду exit и завершается сам - такое развитие событий соответствует нормальному завершению работы программы.
    • Процесс пакетной программы моделирования (foreground)
  2. По завершении работы программы моделирования:
    • Процесс диалога, предоставляющего возможность записи результатов программы в базу данных.
Волшебная кнопка Kill должна по замыслу завершить все процессы, включая родительский - т.е. процесс с малым интерфейсом, по желанию пользователя до завершения работы программы моделирования.

Моим старым решением было удалить все процессы в группе процессов:
catch {exec kill 0}
Эта tcl-команда запускает обычную команду оболочки kill 0, что соответствует посылке сигнала TERM текущей группе процессов. Это решение прекрасно работает в случае, если малый интерфейс запускается из терминала, но, к сожалению, если он был запущен из большого интерфейса, то последний тоже завершается, что не является правильным поведением. Проблема в том, что вся совокупность процессов, запущенная как единая команда из терминала, рассматривается как единая группа процессов. Соответственно, kill 0 убивает все процессы-предки вплоть до того, который был запущен из терминала.

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

Следующим вариантом было:
set ppid [string trim [exec ps -p [pid] --no-headers -o ppid]]
catch {exec kill -TERM -$ppid}
Здесь ищется pid родительского процесса с помощью обычной команды оболочки ps, команда string trim необходима, т.к. ps форматирует вывод и может предварять pid пробелами. В команде kill -TERM -$ppid формальное указание типа сигнала (-TERM) обязательно, т.к. мы используем отрицательное значение pid. Команда kill с отрицательным pid применяется для посылки сигнала всем процессам в группе процессов, соответствующей pid. Фактически, эта команда ничем не отличается от kill 0, кроме возможности формально указывать некоторую группу процессов, а не текущую, но в нашем случае это неважно. Этот вариант так же хорошо работает, если родительский процесс (малый интерфейс) был запущен из терминала, однако в случае с большим интерфейсом последний не завершается как раньше - на этот раз при нажатии на кнопку Kill  просто ничего не происходит. Дело в том, что при запуске малого интерфейса из большого процесс, соответствующий малому интерфейсу не образует группу, так как он был запущен не из терминала. Соответственно, команда kill -TERM -$ppid завершается аварийно, но, поскольку ее вызов обрамлен командой catch, пользователь не видит вообще никакой реакции.

Следующее решение оказалось окончательным и в итоге завершение пакетных программ нажатием на кнопку Kill заработало и в большом интерфейсе. Идея связана с рекурсивным завершением всех подпроцессов родительского процесса, начиная с нижних, включая подпроцессы процесса, активизировавшего завершение, но исключая его самого. После рассылки сигнала завершения всем указанным подпроцессам, посылается сигнал родительскому процессу, а затем процесс, активизировавший завершение, завершает свою работу с помощью exit. Рекурсивная функция rec_kill() выполняет первую часть алгоритма:
proc rec_kill pid {
    set procs [list]
    foreach x [split [exec ps -o pid,ppid ax | \
                           awk "{if ( \$2 == \"$pid\" ) { print \$1 }}"] \
                     "\n"] {
        lappend procs [string trim $x]
    }
    foreach x $procs {
        rec_kill $x
        if {$x != [pid]} {
            catch {exec kill -TERM $x}
        }
    }
    #puts $procs
}
В первом цикле foreach создается список дочерних процессов процесса-аргумента функции pid. Делается это с помощью следующей команды оболочки:
ps -o pid,ppid -ax | awk "{if ( \$2 == \$pid ) { print \$1 }}"
Во втором цикле происходит рекурсивный вызов rec_kill() самой себя и посылка сигнала завершения всем элементам-процессам, найденным в предыдущем foreach, если те не являются собственным процессом. Pid собственного процесса находится с помощью tcl-команды pid, которая стоит в квадратных скобках (этот синтаксический элемент Tcl называется командной подстановкой), обратите внимание, что эта команда в скобках не имеет никакого отношения к одноименному аргументу функции rec_kill().

Теперь в обработчике двойного клика кнопки Kill нужно выполнить следующее:
set ppid [string trim [exec ps -p [pid] --no-headers -o ppid]]
rec_kill $ppid
exec kill -TERM $ppid
exit
После вызова rec_kill() для родительского процесса (который не трогает собственно родительский процесс) посылаем сигнал для завершения родительскому процессу и выходим с помощью exit.

Одно важное замечание. Будьте очень внимательны с удалением родительского процесса. Родительский процесс может завершиться преждевременно и его pid в дочернем процессе изменится (например, станет 1). В нашем случае, если родительским процессом вдруг станет init, то возможны неприятные последствия, такие как неожиданное завершение пользовательской сессии, ввиду последующего каскадного удаления дочерних процессов init.

Update. Еще 2 важных замечания:
Касательно предыдущего замечания: для того чтобы обезопаситься от случайного завершения процесса, неожиданно ставшего родительским, достаточно в дочернем процессе посылать родителю некий пользовательский сигнал вместо TERM, например сигнал USR1, а в родительском процессе (в том, который нужно завершить) определить обработчик для этого сигнала. В Tcl это можно сделать с помощью команды trap из пакета Expect, для подключения которого в начале программы нужно добавить:
package require Expect
Собственно обработчик сигнала USR1:
trap {exit} USR1
Теперь, если родительский процесс завершится по какой-то причине преждевременно, то новый родительский процесс (скорее всего init) будет игнорировать этот сигнал. Однако это не спасает от каскадного удаления его дочерних процессов функцией rec_kill(). В простейшем случае, чтобы обезопаситься от удаления всех процессов сессии, нужно убедиться, что pid родительского процесса не равен 1.

Второе замечание: как было отмечено, родительский процесс (малый интерфейс) запускает по завершении активного процесса (программы моделирования) процесс-диалог для записи результатов в базу данных. Поскольку rec_kill(), которая посылает сигнал завершения программе моделирования вызывается раньше, чем посылка нашего нового сигнала USR1 (или, как раньше, TERM - это не принципиально), то возможен интересный race condition - малый интерфейс может успеть запустить этот процесс-диалог (так как активная задача была завершена, а сам процесс еще нет), а может и не успеть (если планировщик ОС раньше прибьет малый интерфейс после посылки сигнала из дочернего процесса). Чтобы избежать ненужного вызова процесса-диалога, следует сначала убить родительский процесс, а затем вызвать rec_kill() для его подпроцессов. Однако теперь ситуация осложняется тем, что, поскольку rec_kill() определяет дочерние процессы в своем теле, а сигнал завершения уже был послан родительскому процессу, то он может быть уже завершен и нас может ожидать еще один неприятный race condition, при котором rec_kill() не завершит ничего, либо завершит не те процессы. Чтобы избежать такого развития ситуации, нужно получить список дочерних процессов малого интерфейса до посылки ему сигнала USR1. Теперь обработчик двойного клика кнопки Kill будет выглядеть немного неуклюже:
set ppid [string trim [exec ps -p [pid] --no-headers -o ppid]]
if {$ppid == 1} {exit}
set procs [list]
foreach x [split [exec ps -o pid,ppid ax | \
                       awk "{if ( \$2 == \"$ppid\" ) { print \$1 }}"] \
                 "\n"] {
    lappend procs [string trim $x]
}
exec kill -USR1 $ppid
foreach x $procs {
    rec_kill $x
    if {$x != [pid]} {
        catch {exec kill -TERM $x}
    }
}
exit
, но эту неуклюжесть легко устранить введением вспомогательных функций.

среда, 26 января 2011 г.

Простейший пример шаблонного метапрограммирования в C++

Под метапрограммированием обычно понимают предоставление специальных средств внутри или снаружи исходного кода, которые позволяют выводить часть программы автоматически. Таким спецсредством в C++ являются шаблоны (templates). Шаблоны C++ обеспечивают особый метаязык, по своим характеристикам являющийся функциональным и понимаемый исключительно компилятором C++. С функциональными языками шаблонный метаязык сближают наличие общих подходов (сопоставление с образцом, рекурсия) и запрет переменных-состояний.
Рассмотрим следующую ситуацию. Допустим мы хотим создать класс, услуги которого базируются на сходных по назначению, но отличных в их реализации классах BaseA и BaseB. Если оба базовых класса обладают схожим интерфейсом, то логично оформить наш класс в виде шаблона:
    template  < typename  Base >
    class  Derived  : public Base
    {
    };
(Я не буду заострять внимание на содержимом классов, поэтому наш класс пустой, хотя фактически он может содержать множество полей и методов.)
Подразумевается, что вместо шаблонного аргумента Base нужно будет подставлять BaseA либо BaseB в зависимости от требований разработчика. А теперь главный вопрос: что если разработчику необходима информация о базовых классах объектов, основанных на шаблоне Derived. Как ее получить?
На вскидку я могу перечислить четыре способа:
  1. Вполне возможно, что классы BaseA и BaseB сами предоставляют информацию о своих типах. Но это не всегда так, а изменять эти классы мы не можем.
  2. Использование RTTI и dynamic_cast. Во-первых, это не всегда доступно, ведь для использования dynamic_cast необходимо, чтобы BaseA и BaseB были полиморфными, т.е. содержали виртуальные функции. Во-вторых, использование RTTI чревато потерей эффективности.
Оставшиеся два способа связаны с введением в шаблон класса Derived нового метода GetBaseClass(), предоставляющего информацию о базовом типе:
    template  < typename  Base >
    class  Derived : public Base
    {
        public:
            BaseClass  GetBaseClass( void ) const;
    };
BaseClass - это enum, перечисляющий базовые классы, а также значение Prohibited:
    enum  BaseClass
    {
        Prohibited,
        BaseClassA,
        BaseClassB
    };
Итак, оставшиеся способы:
  1. Специализация шаблона Derived для отдельных типов базового класса. К сожалению, шаблон класса Derived может содержать большое число полей и методов, его полная специализация приведет к нежелательному дублированию кода.
  2. Использование трюка, основанного на шаблонном метапрограммировании.
Четвертый способ - это и есть то, что я хотел здесь показать. Он основан на введении вспомогательного шаблона класса (или структуры) BaseClassInstance, не содержащего ничего, кроме статической константы типа BaseClass, и специализированного для разных фактических типов базовых классов:
    template  < typename >
    struct  BaseClassInstance
    {
        static const BaseClass  value = Prohibited;
    };


    template  <>
    struct  BaseClassInstance< BaseA >
    {
        static const BaseClass  value = BaseClassA;
    };


    template  <>
    struct  BaseClassInstance< BaseB >
    {
        static const BaseClass  value = BaseClassB;
    };
(В первом определении после ключевого слова typename можно было указать имя аргумента, например Base, но, поскольку оно не используется в теле шаблона, я его опустил.)
Несмотря на то, что по форме здесь перечисляются разные специализации шаблона структуры C++, по сути, если рассматривать это с точки зрения функционального программирования, здесь объявлены клозы простейшей функции, возвращающей значение типа BaseClass в зависимости от результата сопоставления с образцом (типом - шаблонным аргументом)!
Нам осталось написать тело метода GetBaseClass():
    template  < typename  Base >
    BaseClass Derived< Base >::GetBaseClass( void ) const
    {
        return BaseClassInstance< Base >::value;
    }
Теперь объекты классов, основанных на шаблоне Derived, обладают информацией о своем базовом классе. Эта информация закладывается в момент инстанцирования классов, а значит нет дополнительных нагрузок в ран-тайме, и, что не менее важно, нам не пришлось дублировать исходный код.

пятница, 5 ноября 2010 г.

Реализация парсера пользовательского языка на Haskell

Итак, я выкладываю версию парсера пользовательского языка с поддержкой арифметических выражений с функциями и переменными, написанную на функциональном языке Haskell. С грамматикой языка можно ознакомиться, прочитав мое первое сообщение в этом блоге. Задачей программы, как и прежде, будет построение абстрактного синтаксического дерева (AST) для отдельных инструкций языка. В отличие от версии, написанной на boost::spirit, я не стал реализовывать базовый эвалюатор полученных деревьев, хотя сделать это не сложно.

В качестве лексера и грамматического анализатора я использовал alex и happy соответственно, которые входят в состав Haskell Platform; alex анализирует исходный код лексера (с расширением .x), а happy анализирует исходный код грамматики (с расширением .y). Обе программы генерируют соответствующие файлы с расширением .hs. Фактически, исходные коды лексера и грамматики состоят по большей части из кода на Haskell (все, что находится в этих файлах в фигурных скобках, является валидным кодом Haskell), и лишь небольшое количество мета-объявлений предназначено для предварительной обработки alex и happy.

Описание грамматики языка для happy совместимо с описанием для boost::spirit несмотря на то, что внутренне эти два генератора парсеров реализованы по-разному (happy генерирует LALR парсер, а spirit - парсер с рекурсивным спуском). Хотя проблема левой рекурсии в happy исчезает, и, кроме того, happy предоставляет дополнительные возможности вроде указания приоритета операторов, я не стал изменять реализацию грамматики по сравнению с версией для boost::spirit.

Итак, грамматика нашего пользовательского языка в версии haskell / happy выглядит следующим образом:

Statement : Action Condition { ParseResult $1 $2 }

Action : keep tpt    { KeepTpt }
       | keep edt    { KeepEdt }
       | delete tpt  { DeleteTpt }
       | delete edt  { DeleteEdt }

Condition :: { Subtree }
Condition : if Expression { buildTopTree $2 }

Expression :: { Node }
Expression : OrExpression { $1 }

PrimaryExpression : Function1 { buildFunction1Subtree $1 }
                  | '(' Expression ')' { setPriorityInOperatorTree 0 $2 }
                  | LeafOperand { Leaf $1 }

LeafOperand : Constant { Constant $1 }
            | Variable { Variable $1 }

Constant : double { DoubleValue $1 }
         | integer { IntegerValue $1 }

Variable : identifier '[' integer ',' integer ']' { Vector2 $1 $3 $5 }
         | identifier '[' integer ']' { Vector1 $1 $3 }
         | identifier { Scalar $1 }

Function1 : identifier '(' Expression ')' { Function1 $1 $3 }

OrExpression : AndExpression '|' OrExpression
               { buildOperatorSubtree ( Op ( LogOp Or ) 1 False ) $1 $3 }
             | AndExpression { $1 }

AndExpression : Relation '&' AndExpression
                { buildOperatorSubtree ( Op ( LogOp And ) 2 False ) $1 $3 }
              | Relation { $1 }

Relation : Addition RelOperator Addition
           { buildOperatorSubtree ( Op ( RelOp $2 ) 3 False ) $1 $3 }
         | Addition { $1 }

Addition : Multiplication AddOperator Addition
           { buildOperatorSubtree ( Op ( AddOp $2 ) 4 False ) $1 $3 }
         | Multiplication { $1 }

Multiplication : UnaryExpression MultOperator Multiplication
                 { buildOperatorSubtree ( Op ( MultOp $2 ) 5 False ) $1 $3 }
               | UnaryExpression { $1 }

UnaryExpression : UnaryOperator PrimaryExpression
                  { buildUnaryOperatorSubtree ( Op ( UnaryOp $1 ) 6 True ) $2 }
                | PrimaryExpression { $1 }

UnaryOperator : '-' { UMinus }
              | '!' { Not }

MultOperator : '*' { Mult }
             | '/' { Div }

AddOperator : '+' { Plus }
            | '-' { Minus }

RelOperator : "<=" { LessEqual }
            | ">=" { MoreEqual }
            | "!=" { NotEqual }
            | "<" { Less }
            | ">" { More }
            | "=" { Equal }

happy пользуется определениями токенов из лексера, импортируя его модуль CfLexer. Эти токены (keep, delete, tpt, edt, if, арифметические операторы) сопоставляются с типами лексера и перечислены в секции %token. В фигурных скобках описания грамматики указываются семантические действия, которые должны соответствовать построению объекта необходимого типа, как это сделано и в boost::spirit. Поскольку в Haskell реализован автоматический вывод типов на основе модели Хиндли-Милнера, то формально указывать типы выражений нет необходимости, хотя в двух случаях, для наглядности, я определил типы выражений Condition :: { Subtree } и Expression :: { Node }. Типы выражений определены ниже в этом же файле:


data  Statement =
    ParseResult
    {
        action    :: Action,
        condition :: Subtree
    }

data  Action =
    KeepTpt                  |
    KeepEdt                  |
    DeleteTpt                |
    DeleteEdt
    deriving Show

data  Node =
    Tree Subtree             |
    Leaf LeafOperand
    deriving Show

data  Subtree =
    Subtree
    {
        nodeType :: NodeType,
        children :: [ Node ]
    }
    deriving Show

data  NodeType =
    Operator  Operator       |
    Function  Function
    deriving Show

data  Operator =
    Op
    {
        op         :: OperatorType,
        priority   :: Int,
        hasRLAssoc :: Bool
    }

data  OperatorType =
    Uninitialized            |
    Top                      |
    UnaryOp  UnaryOperator   |
    MultOp   MultOperator    |
    AddOp    AddOperator     |
    RelOp    RelOperator     |
    LogOp    LogOperator

data UnaryOperator =
    UMinus                   |
    Not

data MultOperator =
    Mult                     |
    Div

data AddOperator =
    Plus |
    Minus

data RelOperator =
    LessEqual                |
    MoreEqual                |
    NotEqual                 |
    Less                     |
    More                     |
    Equal

data LogOperator =
    And |
    Or

data  Function =
    FunctionName  String

data  LeafOperand =
    Variable  Variable       |
    Constant  Constant
    deriving Show

data  Constant =
    DoubleValue   Double     |
    IntegerValue  Int

data  Variable =
    Scalar   String          |
    Vector1  String Int      |
    Vector2  String Int Int

data Function1 =
    Function1
    {
        fName :: String,
        fExpr :: Node
    }

Все типы являются алгебраическими в понятии Haskell, то есть создаются с использованием ключевого слова data и могут иметь один и более конструкторов. Некоторые из типов наследуют определения класса Show, что позволит выводить их на экран компьютера. Поскольку дефолтный вывод некоторых типов на экран меня не удовлетворил, я переопределил для них функцию show:


instance Show Operator where
    show = showOperator

showOperator ( Op a _ _ ) = 
    "-op- " ++ case a of
                    Uninitialized   -> "UNINITIALIZED"
                    Top             -> "Top"
                    UnaryOp UMinus  -> "u -"
                    UnaryOp Not     -> "!"
                    MultOp Mult     -> "*"
                    MultOp Div      -> "/"
                    AddOp Plus      -> "+"
                    AddOp Minus     -> "-"
                    RelOp LessEqual -> "<="
                    RelOp MoreEqual -> ">="
                    RelOp NotEqual  -> "!="
                    RelOp Less      -> "<"
                    RelOp More      -> ">"
                    RelOp Equal     -> "="
                    LogOp And       -> "&"
                    LogOp Or        -> "|"

instance Show Function where
    show = showFunction

showFunction ( FunctionName a ) = "-fun- " ++ a

instance Show Variable where
    show = showVariable

showVariable ( Scalar a ) = a
showVariable ( Vector1 a b ) = a ++ "[" ++ show b ++ "]"
showVariable ( Vector2 a b c ) = a ++ "[" ++ show b ++ "," ++ show c ++ "]"

instance Show Constant where
    show = showConstant

showConstant ( DoubleValue a ) = show a
showConstant ( IntegerValue a ) = show a

Окончательный вывод на экран дерева для отдельной инструкции будет осуществляться с помощью функции printResult и ее вспомогательными функциями:


printResult :: Statement -> IO ()
printResult x = do
    putStrLn $ "Result: action = " ++ show ( action x ) ++ ", parsed tree = "
    putStrLn ""
    printSubtree ( condition x ) ""

printSubtree :: Subtree -> String -> IO ()
printSubtree ( Subtree a b ) is = do
    putStrLn $ is ++ case a of
                        Operator aa -> show aa
                        Function aa -> show aa
    printChildren b $ is ++ "    "

printChildren :: [ Node ] -> String -> IO ()
printChildren [] _ = return ()

printChildren ( Tree x : xs ) is = do
    printSubtree x is
    printChildren xs is

printChildren ( Leaf x : xs ) is = do
    putStrLn $ is ++ printLeaf x
    printChildren xs is

printLeaf :: LeafOperand -> String
printLeaf a = case a of
                    Variable aa -> show aa
                    Constant aa -> show aa

При построении AST использовались вспомогательные функции:


buildTopTree :: Node -> Subtree
buildTopTree ( Tree x ) = x
buildTopTree x@( Leaf _ ) =
    Subtree ( Operator $ Op Top 0 False ) [ x ]

buildFunction1Subtree :: Function1 -> Node
buildFunction1Subtree x =
    Tree $ Subtree ( Function . FunctionName $ fName x ) [ fExpr x ]

buildOperatorSubtree :: Operator -> Node -> Node -> Node
buildOperatorSubtree a@( Op _ _ True ) nl nr =
    Tree $ Subtree ( Operator a ) [ nl, nr ]

buildOperatorSubtree a nl nr@( Tree sr@( Subtree ( Operator b ) ( cnrl : _ ) ) )
    | p == priority b =
        Tree $ moveDownLeft ( Subtree ( Operator a ) $
                                      nl : [ getDeepestLeft cnrl p ] ) sr
    | otherwise =
        Tree $ Subtree ( Operator a ) [ nl, nr ]
        where p = priority a

buildOperatorSubtree a nl nr =
    Tree $ Subtree ( Operator a ) [ nl, nr ]

getDeepestLeft :: Node -> Int -> Node
getDeepestLeft x@( Leaf _ ) _ = x

getDeepestLeft x@( Tree ( Subtree ( Operator b ) ( cnl : _ ) ) ) p
    | p == priority b = getDeepestLeft cnl p
    | otherwise = x

getDeepestLeft x _ = x

moveDownLeft :: Subtree -> Subtree -> Subtree
moveDownLeft sl ( Subtree b@( Operator _ ) ( ( Leaf _ ) : cnrr ) ) =
    Subtree b $ Tree sl : cnrr

moveDownLeft sl@( Subtree ( Operator _ ) _ )
             ( Subtree b@( Operator _ )
                       ( ( Tree ( Subtree ( Function _ ) _ ) ) : cnrr ) ) =
    Subtree b $ Tree sl : cnrr

moveDownLeft sl@( Subtree ( Operator a ) _ )
             ( Subtree b@( Operator _ )
                       ( ( Tree csrl@( Subtree ( Operator c ) _ ) ) : cnrr ) )
    | priority a == priority c =
        Subtree b $ Tree ( moveDownLeft sl csrl ) : cnrr
    | otherwise =
        Subtree b $ Tree sl : cnrr

moveDownLeft sl _ = sl

buildUnaryOperatorSubtree :: Operator -> Node -> Node
buildUnaryOperatorSubtree a x = 
    Tree $ Subtree ( Operator a ) [ x ]

setPriorityInOperatorTree :: Int -> Node -> Node
setPriorityInOperatorTree i ( Tree ( Subtree ( Operator ( Op a _ x ) ) y ) ) =
    Tree $ Subtree ( Operator $ Op a i x ) y

setPriorityInOperatorTree _ x = x

Функция buildTopTree проверяет, является ли ее аргумент, представленный типом Node, листом, если это так, то она возвращает простое поддерево (Subtree) с корнем - оператором типа Top и единственным листом - переданным ей аргументом, если же аргумент - дерево, то buildTopTree возвращает его представление типа Subtree. Функция buildOperatorSubtree  и вспомогательные функции getDeepestLeft и moveDownLeft строят поддерево для бинарных операторов и сдвигают лево-ассоциативные операторы (т.е. все бинарные операторы) вправо (т.е. вглубь дерева) относительно операторов с таким же приоритетом: это позволяет исправить правоассоциативные правила грамматики для левоассоциативных операторов. Функция setPriorityInOperatorTree позволяет избежать порчи дерева при сдвиге левоассоциативных операторов для выражений, содержащихся в скобках. Функции buildFunction1Subtree и buildUnaryOperatorSubtree довольно простые и делают то, что от них требует их название.

Это практически все. Осталось упомянуть, что тестовые примеры я поместил в файл cf.hs, а компиляция программы основана на использовании Makefile, поэтому достаточно только распаковать пример и запустить команду make.

Кроме того, хотелось бы сказать несколько слов о сравнительной производительности программ, написанных с использованием boost::spirit и haskell / happy. Я промолчу о скорости компиляции: итак всем понятно, насколько долго будет компилироваться код на C++, использующий шаблонные объявления из boost::spirit. Меня намного больше удивила разница в скорости выполнения откомпилированных программ. Для корректности сравнения я убрал эвалюацию синтаксических деревьев из кода на boost::spirit. Так вот, код на haskell / happy выполнялся на моем компьютере (двухъядерный AMD 64) в среднем порядка 0.04 секунды, код на boost::spirit с включенной оптимизацией -O2: порядка 0.35 сек. (в 10 раз дольше!), а без оптимизации - вообще 3.4 сек., т.е. примерно в 100 раз дольше!

Исходный код примера можно взять здесь.