четверг, 24 декабря 2009 г.

Рекурсивные регулярные выражения aka Контекстно-свободные грамматики

Я недавно уже упоминал, что то, что мы в Perl называем регулярными выражениями, следовало бы называть контекстно-свободно-грамматическими выражениями, но их так никто не называет, наверное, потому что это слишком гадко звучит. Об этом где-то как-то упоминается, но примеров кода, которые это демонстрировали бы, я не встречал. Придется самому исправить это упущение.

Пример грамматики взят из Parsing Techniques (A Practical Guide) by Dick Grune & Ceriel Jacobs, охренительно простой книжки про парсеры — насколько вообще простой может быть охренительно толковая книжка про парсеры. Очень рекомендую.

Итак, задача. Требуется составить регулярное выражение, которое будет совпадать со строкой, состоящей из одинакового количества символов a и b — в любом порядке. Эта задача не имеет решения, которое я и привожу ниже.

use Test::More tests => 31;

=pod

S -> aB | bA
A -> a | aB | bAA
B -> b | bA | aBB

=cut

my ($S, $A, $B);
$S = qr/ ( a (??{$B}) ) | ( b (??{$A}) ) /x;
$A = qr/ ( a ) | ( a (??{$S}) ) | ( b (??{$A}) (??{$A}) ) /x;
$B = qr/ ( b ) | ( b (??{$S}) ) | ( a (??{$B}) (??{$B}) ) /x;

ok('' !~ /^$S$/, 'EMPTY');

ok('a' !~ /^$S$/, 'a');
ok('b' !~ /^$S$/, 'b');

ok('aa' !~ /^$S$/, 'aa');
ok('ab' =~ /^$S$/, 'ab');
ok('ba' =~ /^$S$/, 'ba');
ok('bb' !~ /^$S$/, 'bb');

ok('aaa' !~ /^$S$/, 'aaa');
ok('aab' !~ /^$S$/, 'aab');
ok('aba' !~ /^$S$/, 'aba');
ok('abb' !~ /^$S$/, 'abb');
ok('baa' !~ /^$S$/, 'baa');
ok('bab' !~ /^$S$/, 'bab');
ok('bba' !~ /^$S$/, 'bba');
ok('bbb' !~ /^$S$/, 'bbb');

ok('aaaa' !~ /^$S$/, 'aaaa');
ok('aaab' !~ /^$S$/, 'aaab');
ok('aaba' !~ /^$S$/, 'aaba');
ok('aabb' =~ /^$S$/, 'aabb');
ok('abaa' !~ /^$S$/, 'abaa');
ok('abab' =~ /^$S$/, 'abab');
ok('abba' =~ /^$S$/, 'abba');
ok('abbb' !~ /^$S$/, 'abbb');
ok('baaa' !~ /^$S$/, 'baaa');
ok('baab' =~ /^$S$/, 'baab');
ok('baba' =~ /^$S$/, 'baba');
ok('babb' !~ /^$S$/, 'babb');
ok('bbaa' =~ /^$S$/, 'bbaa');
ok('bbab' !~ /^$S$/, 'bbab');
ok('bbba' !~ /^$S$/, 'bbba');
ok('bbbb' !~ /^$S$/, 'bbbb');

Вопросы есть?

Update: спасибо zloy-russkiy за ссылку на Regexp::Grammars для 5.10!
Update #2: спасибо Павлу Кудинову за его собственную реализацию парсера на рекурсивных регулярных выражениях для 5.8.8!

14 comments:

  1. Пробовал написать парсер для языков, определяемых контекстно-свободными грамматиками, с использованием родного перлового движка "регулярных" выражений - не получилось :-( А именно, не получается поставить обработчик на захваченные карманы из-за откатов :-( Может, кто-то сможет придумать, что с этим можно сделать?

    Кстати, в Perl 6 будет уже встроенный тип данных grammar. Правда, еще с ним не разбирался.
    ОтветитьУдалить
  2. P. S. Невозможность повесить обработчик на захваченный карман внутри рекурсивного "регулярного" выражения наносит сокрушительный удар по их полезности.
    ОтветитьУдалить
  3. P. P. S. Я, наверное, слишком витиевато выражаю свои мысли. Короче, я хотел сказать, что хотя проверить, удовлетворяет ли грамматике заданная строка, легко, построить дерево синтаксического разбора у меня не получилось.
    ОтветитьУдалить
  4. Regexp::Grammars разве не то что нужно?
    ОтветитьУдалить
  5. YAHOO!!! Дамиан везде поспел! Спасибо, не знал про этот модуль!
    ОтветитьУдалить
  6. у меня в 2007 получилось сделать карманы и построить дерево

    http://kudinov.name/regexml.zip

    по ссылке работающий XML Parser (одной большой регэкспой разбивает документ на структурированное дерево) и Parser моего шаблонизатора (для него и писался), и исходники.

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

    1. имеем диалект которым описываем например формат XML
    2. компилируем это в совокупность регулярных выражений, получается perl код
    3. применяем perl код к строке - он выдаёт структурированное дерево - результат разбора "с карманами"

    т.к. диалект - валидный XML документ, "компилятор" из диалекта в перл-код использует для парсинга XML простенький XML парсер, работающий на перл-коде, полученном из описания XML формата диалектом. для первой компиляции (bootstrap) использовал XML::Parser, т.к. формат вывода один и тот же.

    p.s. работает в perl 5.8.8+, в 5.10 регэкспы ведут себя иначе, не портировал
    ОтветитьУдалить
  7. Спасибо за коммент! Ответил в твоем журнале.
    ОтветитьУдалить
  8. Эх... А я теперь мучаюсь от тщетной гордыни, что не допереизобрёл этот велосипед самостоятельно. Ведь мысли были о том же: и чтобы эксплойтнуть поведение local при откатах, и чтоб потом сделать bootstrap для грамматики самого парсера...

    Но, видимо, в то время - а это я свои старые черновики поднял, года полтора назад уже как прошло - в то время не было времени (или суровой необходимости), а сейчас чего-то не подумав, сразу в дневник ляпнул :-( Вот за что мне нравится www.braingames.ru - это за то, что они, с&ки, никогда ответы не говорят :-)

    А вообще, сама задача отличная. Буду такую на собеседованиях задавать :-) Когда точно знаешь, что решение есть, велосипеды, как правило, гораздо проще изобретаются :-)
    ОтветитьУдалить
  9. А гордыня-то ложная :-D

    Caught somewhere in the middle are the people who have a balanced
    view of how much abstraction is good, but who jump the gun on writing
    their own abstractions when they should be reusing existing code.
    -- Larry Wall "Programming Perl"

    Ну тогда и не буду больше расстраиваться :-)
    ОтветитьУдалить
  10. Что-то меня сегодня прёт нереально, я эту задачу ОЧЕНЬ ПРОСТО РЕШИЛ. Правда при помощи Perl >5.10.0:

    perl -E "$a='aaabbbb'; say $1,' ',$2 if $a =~ /(a+)(b+)(?(?{ length $1 != length $2 })(*FAIL))/;"

    Собственно результат:
    aaa bbb
    ОтветитьУдалить
  11. уфффффф!!!! Теперь для ранних версий Perl, причём полностью соответствующее условию задачи:

    $a='bbbaabcccaaabbbb';
    my @results = $a =~ /(?:(a+b+)|(b+a+))(?(?{ my $s = $1 || $2; index($s,substr($s,-1,1))*2 != length($s) })(?>
    ^))/g;
    printf "%s\n",join qq(\n),@results;

    Результат:
    bbaa
    aaabbb

    Конечно же там ещё есть ab, но в условии задачи нет условий для разрешения такого рода конфликтов.

    P.S. Perl - самое лучшее что можно было придумать для пытливых умов.
    ОтветитьУдалить
  12. Ну и конечно же идеологически верный вариант для Perl >=5.10.0:

    perl -E "$a='baaabbbb'; say $1,' ',$2 if $a =~ /(?|(a+)(b+)|(b+)(a+))(?(?{ length $1 != length $2 })(*FAIL))/;"
    ОтветитьУдалить
  13. Это, конечно, чит. Можно с тем же успехом написать

    perl -le '$_ = shift; $a = tr/a/a/; $b = tr/b/b/; print $a == $b && $a + $b == length' <строка>

    Кстати, по условии символы a и b могут идти в любом порядке, в том числе и в перемешку.

    В случае же, если должны идти сначала все буквы "a", и только потом все буквы "b", либо наоборот, мы имеем дело с контекстно-зависимой грамматикой (если моя не врёт) и даже "рекурсивные регулярные выражения" тут бессильны.
    ОтветитьУдалить