Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 23.01.09 15:26
Оценка:
Насколько правильным было бы использовать такую структуру приложения:

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

Допустим, мы считываем операции какого-либо ассемблера.

Насколько красиво выглядел бы такой подход к считыванию: создаются отдельные классы, каждый класс расчитан на то, чтобы заполнить объект типа Oper по ассемблеру конкретной операции. Например, для операции call — class CallFiller и т.п. Но каждый такой класс умеет по строке отличить, его ли это тип данных.

Реализовать такое можно было бы также по-разному.

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

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

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

Насколько такой подход адекватен?
Re: Парсер: свой класс для каждого типа входных строк
От: ivb22  
Дата: 23.01.09 16:28
Оценка:
Здравствуйте, Аноним, Вы писали:

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


А>Насколько такой подход адекватен?


Есть достаточно обширная область "программистского" знания, касающаяся программирования компиляторов.
Одно дело это распарсить простенький DSL, но даже ассемблер микроконтроллера может стать непосильно задачей для решения её "в лоб".
По крайней мере, советую хотя бы поверхностно овладеть теорией вопроса.
Начать знакомиться можно отсюда
Re[2]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 23.01.09 17:28
Оценка:
I>Начать знакомиться можно отсюда

Речь идёт не о конкретном примере, а о том, как Вы относитесь к конкретному подходу.

Понятно, что для фронт-энда реального компилятора лучше использовать yacc, чем зря писать кучу кода. Здесь задача намного проще.

Ассемблер пусть какой-нибудь сильно упрощённый, где всего десяток почти ничем не связанных операций.
Re[3]: Парсер: свой класс для каждого типа входных строк
От: ivb22  
Дата: 23.01.09 23:18
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Ассемблер пусть какой-нибудь сильно упрощённый, где всего десяток почти ничем не связанных операций.


Если совсем упрощённый, я бы сделал тупо через рефлексию
string fillerName = command.Split(new char[] { ' ' })[0] + "Filler";
Type fillerType = Type.GetType(fillerName);
Filler concreteFiller = (Filler)Activator.CreateInstance(fillerType);

(код — условный)
Re: Парсер: свой класс для каждого типа входных строк
От: Юрий Жмеренецкий ICQ 380412032
Дата: 24.01.09 07:35
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Насколько правильным было бы использовать такую структуру приложения:


А>Имеется один тип данных (Oper), объекты которого считываются из файла. Однако, в файле они представлены существенно по-разному.


А>Допустим, мы считываем операции какого-либо ассемблера.


А>Насколько красиво выглядел бы такой подход к считыванию: создаются отдельные классы, каждый класс расчитан на то, чтобы заполнить объект типа Oper по ассемблеру конкретной операции. Например, для операции call — class CallFiller и т.п. Но каждый такой класс умеет по строке отличить, его ли это тип данных.


А>Реализовать такое можно было бы также по-разному.


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


По хорошему — для каждого нетерминала надо делать свой КА и потом объединять их в один. Тогда результат будет получен за один проход, без откатов при неудачных попытках.
Re[2]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 24.01.09 09:11
Оценка:
Здравствуйте, Юрий Жмеренецкий, Вы писали:

ЮЖ>По хорошему — для каждого нетерминала надо делать свой КА и потом объединять их в один. Тогда результат будет получен за один проход, без откатов при неудачных попытках.


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

Задача скорее заключается в максимально ленивом написании кода. Честно сказать, я даже задумывался о том, чтобы организовать (если что) проход шаблонно-рекурсивной функции в виде двоичного поиска — чтобы нивелировать откаты -, только на этапе генерации она бы раскрылась не в N функций, а в N*logN и тут ещё нужно было бы заботиться о порядке комманд
Re[3]: Парсер: свой класс для каждого типа входных строк
От: Юрий Жмеренецкий ICQ 380412032
Дата: 24.01.09 11:05
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Здравствуйте, Юрий Жмеренецкий, Вы писали:


ЮЖ>>По хорошему — для каждого нетерминала надо делать свой КА и потом объединять их в один. Тогда результат будет получен за один проход, без откатов при неудачных попытках.


А>Да, задача эффективного считывания тут настолько мало важна...

(*1*)

А>Задача скорее заключается в максимально ленивом написании кода. Честно сказать, я даже задумывался о том, чтобы организовать (если что) проход шаблонно-рекурсивной функции в виде двоичного поиска — чтобы нивелировать откаты -, только на этапе генерации она бы раскрылась не в N функций, а в N*logN


Имхо, это пртиворечит (*1*)
Re[4]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 25.01.09 10:11
Оценка:
Здравствуйте, Юрий Жмеренецкий, Вы писали:

ЮЖ>Имхо, это пртиворечит (*1*)


Да, я как раз и хотел сказать, что настолько не важна была эффективность, что когда я задумался, что вдруг понадобится, мне пришла в голову эта идея про двоичный поиск
Re: Парсер: свой класс для каждого типа входных строк
От: Кирилл Лебедев Россия http://askofen.blogspot.com/
Дата: 25.01.09 11:57
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Насколько правильным было бы использовать такую структуру приложения:

А>Имеется один тип данных (Oper), объекты которого считываются из файла. Однако, в файле они представлены существенно по-разному.
Тут вот в чём проблема. Какие обязанности будут у класса Oper и его производных? Что он будет делать? За что будет отвечать? Если у класса не будет обязанностей, то он бесполезен. Поскольку он ничего не делает. Если же обязанности будут "навешиваться" ему произвольным образом (т.е. класс станет супер-универсальным, что-то вроде "и жрец, и жнец, и на дуде игрец"), то у Вас получится спагетти-код — код, который невозможно сопровождать.

Поэтому первоочередная задача — это определение обязанностей. Причём правильнее её выполнять таким образом:

  1. Сначала найти обязанности.
  2. Затем делегировать их конкретным классам/модулям.

А>Насколько такой подход адекватен?

Мне кажется, что при решении данной задачи нужно не изобретать свой подход, а следовать теории компиляторов, которая создана давно и уже доказала свою эффективность.

Согласно теории, процесс компиляции состоит из следующих этапов:

  1. Лексический анализ.
  2. Синтаксический анализ.
  3. Семантический анализ.
  4. Оптимизация.
  5. Генерация кода.

Если следовать теории, то у Вас в программе должны появиться такие сервисные классы (или модули):


Помимо сервисных классов у Вас должны появиться вспомогательные классы:


Вполне вероятно, что также понадобится добавить структуры:


Но основной набор классов вряд ли как-то изменится. Теперь в соответствии со спецификой задачи классы нужно конкретизировать. В простых структурах вроде Лексемы или Команды нужно определить поля. Для абстрактных типов данных вроде Дерева разбора или Дополненное дерево разбора нужно посмотреть реализации в справочниках по алгоритмам и АТД. Для сервисных классов нужно определить прототипы основных операций и подумать над их реализацией.
С уважением,
Кирилл Лебедев
Software Design blog — http://askofen.blogspot.ru/
Re[2]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 25.01.09 23:01
Оценка:
С теорией компиляторов я более или менее знаком, вопрос, собственно, не компиляторов касался, просто мне показалось, что для иллюстрации такой пример бы подошёл.

С компиляторами всё чаще приходится дело иметь с модулями, а не классами, так как работа на Си ведётся чаще, как мне кажется. Хотя можно и в Си изобретать "классы" и работать ими, ну, или реализовывать в Си функции, которые реализует, например, наследование в плюсах и т.п.
Ушёл от темы.

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

Хотя мне сложно представить, чтобы для разных операций вводились разные классы, так как опять таки сталкивался с операционным представлением только на Си приходилось. Но, наверное, было бы в этом определённое удобство.

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


Теперь свой изначальный пример мне всё больше хочется сравнить вот с чем, имеются различные виды записи одного и того же. Например, того же бинарника или его дизассемблера. Вполне нормально иметь две оболочки для считывания, а сохраняются они в одно и то же операционное представление. Вот и весь вопрос в чём был, можно было и не задавать

Про универсальность Oper думал, собственно, думал, может сразу то и считывать каждую операцию своим классом заполнения в свой класс операции
Re[2]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 25.01.09 23:18
Оценка:
Здравствуйте, Кирилл Лебедев, Вы писали:


КЛ>Но основной набор классов вряд ли как-то изменится. Теперь в соответствии со спецификой задачи классы нужно конкретизировать. В простых структурах вроде Лексемы или Команды нужно определить поля. Для абстрактных типов данных вроде Дерева разбора или Дополненное дерево разбора нужно посмотреть реализации в справочниках по алгоритмам и АТД. Для сервисных классов нужно определить прототипы основных операций и подумать над их реализацией.



Да ещё будет кучка аналитических и семантических графов: операционное представление, поток данных, поток управления, граф зависимостей, маски используемых и записываемых битов для пропагации констант, дерево доминаторов, дерево циклов... что ещё
Re: Парсер: свой класс для каждого типа входных строк
От: Pzz Россия https://github.com/alexpevzner
Дата: 26.01.09 02:50
Оценка:
Здравствуйте, Аноним, Вы писали:

А>Насколько красиво выглядел бы такой подход к считыванию: создаются отдельные классы, каждый класс расчитан на то, чтобы заполнить объект типа Oper по ассемблеру конкретной операции. Например, для операции call — class CallFiller и т.п. Но каждый такой класс умеет по строке отличить, его ли это тип данных.


Мне кажется, говоря на примере ассемблера, что это не самый удачный подход. Дело в том, что самий операций несколько сотен, но они распадаются на 1-2 десятка типов, очень похожих с точки зрения синтаксиса (и тип определяется, в одновном, тем, как надо разбирать аргументы).

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


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

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


Для написания парсеров есть специальные инструменты, типа lex/yacc, которые программируются в терминах грамматики. Что гораздо удобнее вашего подхода.

В boost'е, говорят, есть какой-то эквиавалент yacc'а, позволяющий описывать грамматику, и реализованный на темплейтах.
Re[2]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 26.01.09 10:08
Оценка:
Здравствуйте, Pzz, Вы писали:



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


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




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

К тому же на всякий случай я продумал более эффективный вариант обхода двоичным поиском, что немного более криво и геморно, и требует N*logN функций, сгенерированных из одного рекурсивного шаблона против N в линейном поиске.

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

Поэтому я в итоге задал данный вопрос.

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


Но опять таки тут можно вспомнить из приведённых вариантов решения первый, а затем и второй...

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

Опять таки — у меня есть хедер, где я при необходимости создаю новый класс, и есть другое место, где данный класс надо куда-то добавлять... Хотя, наверное, можно поступить и как-нибудь более хитро... Но возможность обратиться статически по константе номеру класса к нему самому, чтобы сделать new, например, мне позволит не лазить в другие участки кода после добавления соответствующего класса.

Вот критика такого метапрограммного подхода меня даже больше всего интересует.
Re[3]: Парсер: свой класс для каждого типа входных строк
От: Кирилл Лебедев Россия http://askofen.blogspot.com/
Дата: 26.01.09 12:32
Оценка:
Здравствуйте, Аноним, Вы писали:

А>С компиляторами всё чаще приходится дело иметь с модулями, а не классами, так как работа на Си ведётся чаще, как мне кажется. Хотя можно и в Си изобретать "классы" и работать ими, ну, или реализовывать в Си функции, которые реализует, например, наследование в плюсах и т.п.

Класс — это и есть специализированный модуль... с возможностью создания экземпляра.

А>В одиночку браться за реализацию всего от лексического, синтаксического анализаторов до машинно-зависимых оптимизаций и кодогенерации — это вызов

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

ИМХО, теория компиляторов задаёт направление, в котором нужно "копать". А адекватность того или иного технического решения поставленной задачи целиком и полностью лежит на программисте. Т.е., я к тому, что вовсе необязательно писать оптимизатор, если он не нужен. Да и лексический и синтаксический анализаторы для простой задачи (грамматики) не будут такими сложными, как кажется со стороны. Более того, отказавшись от следования теории, Вы всё равно в том или ином виде напишите и лексический, и синтаксический анализаторы. Только вместо грамотного разделения, они будут у Вас помещены в один модуль (класс). Это только добавит путаницу и затруднит как само написание кода, так и его сопровождение.
С уважением,
Кирилл Лебедев
Software Design blog — http://askofen.blogspot.ru/
Re[4]: Парсер: свой класс для каждого типа входных строк
От: Аноним  
Дата: 26.01.09 12:42
Оценка:
Здравствуйте, Кирилл Лебедев, Вы писали:

КЛ>ИМХО, теория компиляторов задаёт направление, в котором нужно "копать". А адекватность того или иного технического решения поставленной задачи целиком и полностью лежит на программисте. Т.е., я к тому, что вовсе необязательно писать оптимизатор, если он не нужен. Да и лексический и синтаксический анализаторы для простой задачи (грамматики) не будут такими сложными, как кажется со стороны. Более того, отказавшись от следования теории, Вы всё равно в том или ином виде напишите и лексический, и синтаксический анализаторы. Только вместо грамотного разделения, они будут у Вас помещены в один модуль (класс). Это только добавит путаницу и затруднит как само написание кода, так и его сопровождение.



кстати, по поводу грамматик и ассемблера. дерево синтаксического разбора при распарсивании ассемблерного файла мне представляется только, когда используются макроопределения и т.п.

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

может, я этого просто не вижу?
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.