После прочтения книги про регулярные выражения (далее просто РВ) у меня появились кое-какие мысли по поводу их читаемости. Когда РВ только появлялись, и в них было довольно мало условных обозначений вроде \d, \w и тому подобных, то, наверное, все было не так страшно, хотя уже тогда стоило задуматься о наглядности. Сейчас чтение кода с РВ — это тихий ужас. Нет, если РВ короткое, то особых проблем нет, но по мере их усложнения и появления различных скобок все становится просто кошмарно. Ситуация усугубляется тем, что в некоторых языках (не будем указывать пальцем) постоянно приходится удваивать слеши.
Кроме того в той нотации РВ, которая сейчас используется в большинстве языках программирования, в некоторых, казалось бы простых ситуациях, приходится выкручиваться с помощью различных финтов. Первый пример, что пришел в голову — составить регулярное выражение если «abc», то затем НЕ «xyz».
На мой взгляд пора уже отказаться от той нотации, которая сейчас используется, и создать новую, которая будет ближе к обычному языку программирования, ведь нотация РВ — это по сути и есть язык, но оформленный просто ужасно. Самое худшее, что есть в сегодняшней нотации — это обилие скобок вроде (...), (:...), (?:...), (?=...), (?!...), (?<=...), (?<!...), (?<). Именно благодаря ним выражения становятся запутанными и невозможно охватить взглядов все РВ, чтобы сразу сказать, что оно ищет, а приходится проверять каждый символ в строке, не забыв, например, что ^ в середине РВ — это начало строки, а в начале квадратных скобок [^...] — это инверсия. Ну неправильно то, что при появлении новой возможности разработчики создают новое обозначение, какое-нибудь (&^%$#@...), которое само по себе абсолютно ничего не говорит.
Ведь в чем прелесть обычных языков программирования (не беря какие-то крайние случаи)? Если мы видим оператор if или whileв незнакомом языке, то можем сразу сказать, что он примерно он делает. Да, можно заменить эти операторы на символы вроде @#$% и #&$^ соответственно, к ним можно даже привыкнуть, но как говорилось в анекдоте про урок русского языка в Грузии, «это надо запомнить, понять этого невозможно».
Возможно, ситуацию могли бы улучшить умные редакторы кода, которые в регулярном выражении по-разному подсвечивали бы скобки (?:...), (?=...) и т.д., чтобы сразу видеть области их действий, но для большинства языков программирования этого сделать почти невозможно, т.к. РВ там — это строка и редактор должен был бы уметь определять по содержимому строки, что перед ним: РВ или обычный текст. Да и все-равно при большой вложенности скобок РВ превратится в разноцветную радугу.
Вообще говоря, довольно неплохие изменения в плане читаемости РВ уже произошли благодаря появлению режимов, когда РВ записываются на нескольких строках, а также благодаря комментариям внутри РВ. Появилась даже конструкция с понятным видом (?if...), в Perl (не к ночи будет упомянут) в регулярное выражение можно встраивать программный код, а в .NET вместо простой замены по РВ заменяемое значение может генериться с помощьюспециально обученного специального делегата. В целом уже можно даже написать более-менее понятное РВ, но все-равно это не то, это больше похоже на костыли.
Уже пора создать язык РВ, похожий на остальные «человеческие» языки программирования, а не на Brainfuck. Тогда в нем можно было бы организовать понятную подсветку, подсказки а ля IntelliSense, а в будущем, возможно, и пошаговую отладку РВ.
Дальше хотелось бы показать какими бы мне хотелось видеть РВ.
Во-первых, их надо как-то отделить РВ от обычных строк. Понятно, что функции для их работы требуют именно строк, не уверен, что РВ стоит встраивать в сами языки, как это сделано в Perl, пусть остаются строками, но чтобы их как-то выделить внутри кавычек стоит использовать какие-нибудь дополнительные обозначения. Это может быть что угодно, например, вместо "\d\w" (для наглядности я не буду удваивать слеши) стоит использовать "!\d\w!" или "<\d\w>", тогда редактор сможет легко отличить РВ от строк. В дальнейшем я буду использовать запись "!...!", но это не важно, как и остальные обозначения, главное суть.
Во-вторых, РВ нужно записывать только в режиме, когда игнорируются пробелы и переводы строк, причем, чтобы отделить внутри выражения литералы, которые всегда остаются неизменными от конструкций самих РВ, литералы можно брать в кавычки (не важно какие). Например, вместо «abcd\d\wxyz» можно будет написать:
Или даже "!'abcd' \d\w 'xyz'!"
Редактор кода здесь отдельно сможет подкрасить abcd и xyz. Возможно, стоит использовать знак "+", чтобы связать эти части. Так даже будет нагляднее: "!'abcd' + \d\w + 'xyz'!", т.к. отдельные части РВ больше разделяются визуально.
Вас может смутить то, что знак "+" сейчас используется в значении «1 или больше совпадений», но это не страшно, потому что в этом значении его никто использовать больше не собирается. Это же не логично. Есть же такие наглядные конструкции как {min, max}, давайте их использовать вместе с оператором "*". Оператор "*" стоит использовать как раз в значении «умножить», то есть выражение "!'abc' * 3!" означает, что строка 'abc' должна повториться 3 раза. РВ "!'abc' * {1, 3}!" означает, что 'abc' должна повториться от 1 до 3 раз. Аналогично можно использовать запись "!'abc' * {1, }!" в значении «1 и больше совпадений» вместо "+", а вместо оператора "*" писать: "!'abc' * {0, }!". А запись "!'abc' * {3, 3}!" равносильна той, что мы уже видели "!'abc' * 3!". Старый оператор "*" тогда будет заменен на выражение "!'abc' * {,} !".
Возможно, вместо фигурных скобок стоит использовать квадратные или круглые, тогда это будет даже ближе к математической записи отрезков и интервалов.
Остается вопрос с тем, как обозначить оператор минимальный "*" (он же не жадный). Можно было бы использовать оператор деления, но это тоже не логично, поэтому можно записать прямо в виде "!'abc' min* 3 !". Здесь min* — это один оператор без пробела. Этот вариант записи мне не очень нравится, но по крайней мере он своим именем поясняет суть.
Большинство скобок стоит заменить на встроенные функции. Например, вместо "[abc]" стоит записывать в виде "!any (a, b, c)!", тогда можно будет таким же образом заменить выражение "(:abc)|(:xyz)" на "! any ('abc', 'xyz') !" и мы сможем избавиться еще и от оператора "|". В качестве параметров функции можно использовать РВ, например "! any (\d\w, 'abc') !".
Надо решить как поступать с простейшими выражениями вроде \w, \b, \d и т.п. С одной стороны, они довольно компактные, но мне, например, нравится запись, которая сейчас может использоваться в квадратных скобках — [:alnum:]. Для удобства можно заменить их на запись вида _alnum_. А может быть самые простейшие \d и \w стоит оставить как есть. А вместо ".", которая не особо видна в тексте можно использовать запись _any_. Те же пробелы и табуляции, которые игнорируются в самом выражении можно записывать в виде _space_ или просто брать их в кавычки.
Обязательно нужно ввести нормальный оператор if — then — else, суть которого заключается в том, что если выражение после if выполняется, то затем проверяется РВ в ветке then, иначе после ветки else. Думаю, что слово then можно опустить. Тогда можно будет составить такое РВ:
Здесь я использовал синтаксис как в С-подобных языках, но это не критично. Дословно это выражение обозначает: Сначала идет строка 'abc', затем проверяется РВ '\w * 3', если оно выполняется, то затем должно идти 'xyz', в противном случае должно идти как минимум одно число, а затем 'klmn'.
Может быть, даже стоит ввести операторы типа case, while и for. Кроме того нужно ввести логические операции И, ИЛИ, НЕ, чтобы их использовать в условии. Не уверен на счет И и ИЛИ, ведь выражение "! if ('abc' && 'xyz') !" равносильно "! if ('abcxyz') !", а "! if ('abc' || 'xyz') !" — "! if (any ('abc', 'xyz') ) !". Но оператор отрицания нужен точно, чтобы определять что в данном месте не должно находиться.
Нужно ввести переменную, которая обозначает позицию в строке, где сейчас осуществляется поиск (пусть будет переменная _pos_), а так же переменная, хранящая саму строку, к которой применяется РВ (_this_). Тогда оператор "^" можно заменить более понятным "! _pos_ == 0 !", а "$" на "! _pos_ == (strlen(_this_) — 1) !" Может быть стоит ввести отдельное обозначение для конца строки, например, по аналогии с Python: _pos_ == -1. Эти же переменные позволят сделать опережающую и ретроспективную проверку.
Нужно оставить комментарии. Как они будут выглядеть это уже не важно.
Оператор присваивания должен работать в двух режимах. Первый — это проверка и присваивание переменной строки, соответствующей регулярному выражению, то, для чего сейчас используется запись вроде "(?<foo>...)": "!foo = \w\d*; !". Точку с запятой придется использовать, чтобы показать где кончается оператор присваивания.
Второй режим присваивания — сохранение регулярного выражения без его проверки. Используется для наглядности, например,
Здесь выражение !\d\w*! (обратите внимание на восклицательные знаки) используется затем по имени переменной foo.
Это основные идеи, которые появились относительно РВ. Было бы интересно попробовать такие выражения в деле, но, к сожалению, руки до реализации такого парсера у меня вряд ли дойдут. А вообще, начать можно было бы с того, что такие выражения преобразовывались к классическому виду РВ, а затем обрабатывались бы готовой библиотекой.
В завершении маленький пример для поиска URL. Возможно, там не все учел, например, считается, что доменная зона может быть только com, net, info или двухбуквенная.
Надеюсь, что нигде не ошибся, но даже если и ошибся, не страшно, главное хотелось показать суть.
В завершении еще раз скажу, что главной целью всего этого было придумать как можно повысить читаемость РВ. Разумеется, при этом объем набираемого текста увеличится, но при больших РВ оно того стоит.
Кроме того в той нотации РВ, которая сейчас используется в большинстве языках программирования, в некоторых, казалось бы простых ситуациях, приходится выкручиваться с помощью различных финтов. Первый пример, что пришел в голову — составить регулярное выражение если «abc», то затем НЕ «xyz».
На мой взгляд пора уже отказаться от той нотации, которая сейчас используется, и создать новую, которая будет ближе к обычному языку программирования, ведь нотация РВ — это по сути и есть язык, но оформленный просто ужасно. Самое худшее, что есть в сегодняшней нотации — это обилие скобок вроде (...), (:...), (?:...), (?=...), (?!...), (?<=...), (?<!...), (?<). Именно благодаря ним выражения становятся запутанными и невозможно охватить взглядов все РВ, чтобы сразу сказать, что оно ищет, а приходится проверять каждый символ в строке, не забыв, например, что ^ в середине РВ — это начало строки, а в начале квадратных скобок [^...] — это инверсия. Ну неправильно то, что при появлении новой возможности разработчики создают новое обозначение, какое-нибудь (&^%$#@...), которое само по себе абсолютно ничего не говорит.
Ведь в чем прелесть обычных языков программирования (не беря какие-то крайние случаи)? Если мы видим оператор if или whileв незнакомом языке, то можем сразу сказать, что он примерно он делает. Да, можно заменить эти операторы на символы вроде @#$% и #&$^ соответственно, к ним можно даже привыкнуть, но как говорилось в анекдоте про урок русского языка в Грузии, «это надо запомнить, понять этого невозможно».
Возможно, ситуацию могли бы улучшить умные редакторы кода, которые в регулярном выражении по-разному подсвечивали бы скобки (?:...), (?=...) и т.д., чтобы сразу видеть области их действий, но для большинства языков программирования этого сделать почти невозможно, т.к. РВ там — это строка и редактор должен был бы уметь определять по содержимому строки, что перед ним: РВ или обычный текст. Да и все-равно при большой вложенности скобок РВ превратится в разноцветную радугу.
Вообще говоря, довольно неплохие изменения в плане читаемости РВ уже произошли благодаря появлению режимов, когда РВ записываются на нескольких строках, а также благодаря комментариям внутри РВ. Появилась даже конструкция с понятным видом (?if...), в Perl (не к ночи будет упомянут) в регулярное выражение можно встраивать программный код, а в .NET вместо простой замены по РВ заменяемое значение может генериться с помощью
Уже пора создать язык РВ, похожий на остальные «человеческие» языки программирования, а не на Brainfuck. Тогда в нем можно было бы организовать понятную подсветку, подсказки а ля IntelliSense, а в будущем, возможно, и пошаговую отладку РВ.
Дальше хотелось бы показать какими бы мне хотелось видеть РВ.
Во-первых, их надо как-то отделить РВ от обычных строк. Понятно, что функции для их работы требуют именно строк, не уверен, что РВ стоит встраивать в сами языки, как это сделано в Perl, пусть остаются строками, но чтобы их как-то выделить внутри кавычек стоит использовать какие-нибудь дополнительные обозначения. Это может быть что угодно, например, вместо "\d\w" (для наглядности я не буду удваивать слеши) стоит использовать "!\d\w!" или "<\d\w>", тогда редактор сможет легко отличить РВ от строк. В дальнейшем я буду использовать запись "!...!", но это не важно, как и остальные обозначения, главное суть.
Во-вторых, РВ нужно записывать только в режиме, когда игнорируются пробелы и переводы строк, причем, чтобы отделить внутри выражения литералы, которые всегда остаются неизменными от конструкций самих РВ, литералы можно брать в кавычки (не важно какие). Например, вместо «abcd\d\wxyz» можно будет написать:
"! 'abcd'
\d\w
'xyz'
!"
Или даже "!'abcd' \d\w 'xyz'!"
Редактор кода здесь отдельно сможет подкрасить abcd и xyz. Возможно, стоит использовать знак "+", чтобы связать эти части. Так даже будет нагляднее: "!'abcd' + \d\w + 'xyz'!", т.к. отдельные части РВ больше разделяются визуально.
Вас может смутить то, что знак "+" сейчас используется в значении «1 или больше совпадений», но это не страшно, потому что в этом значении его никто использовать больше не собирается. Это же не логично. Есть же такие наглядные конструкции как {min, max}, давайте их использовать вместе с оператором "*". Оператор "*" стоит использовать как раз в значении «умножить», то есть выражение "!'abc' * 3!" означает, что строка 'abc' должна повториться 3 раза. РВ "!'abc' * {1, 3}!" означает, что 'abc' должна повториться от 1 до 3 раз. Аналогично можно использовать запись "!'abc' * {1, }!" в значении «1 и больше совпадений» вместо "+", а вместо оператора "*" писать: "!'abc' * {0, }!". А запись "!'abc' * {3, 3}!" равносильна той, что мы уже видели "!'abc' * 3!". Старый оператор "*" тогда будет заменен на выражение "!'abc' * {,} !".
Возможно, вместо фигурных скобок стоит использовать квадратные или круглые, тогда это будет даже ближе к математической записи отрезков и интервалов.
Остается вопрос с тем, как обозначить оператор минимальный "*" (он же не жадный). Можно было бы использовать оператор деления, но это тоже не логично, поэтому можно записать прямо в виде "!'abc' min* 3 !". Здесь min* — это один оператор без пробела. Этот вариант записи мне не очень нравится, но по крайней мере он своим именем поясняет суть.
Большинство скобок стоит заменить на встроенные функции. Например, вместо "[abc]" стоит записывать в виде "!any (a, b, c)!", тогда можно будет таким же образом заменить выражение "(:abc)|(:xyz)" на "! any ('abc', 'xyz') !" и мы сможем избавиться еще и от оператора "|". В качестве параметров функции можно использовать РВ, например "! any (\d\w, 'abc') !".
Надо решить как поступать с простейшими выражениями вроде \w, \b, \d и т.п. С одной стороны, они довольно компактные, но мне, например, нравится запись, которая сейчас может использоваться в квадратных скобках — [:alnum:]. Для удобства можно заменить их на запись вида _alnum_. А может быть самые простейшие \d и \w стоит оставить как есть. А вместо ".", которая не особо видна в тексте можно использовать запись _any_. Те же пробелы и табуляции, которые игнорируются в самом выражении можно записывать в виде _space_ или просто брать их в кавычки.
Обязательно нужно ввести нормальный оператор if — then — else, суть которого заключается в том, что если выражение после if выполняется, то затем проверяется РВ в ветке then, иначе после ветки else. Думаю, что слово then можно опустить. Тогда можно будет составить такое РВ:
"! 'abc'
if (\w * 3)
{
'xyz'
}
else
{
\d * {1, } 'klmn'
}
!"
Здесь я использовал синтаксис как в С-подобных языках, но это не критично. Дословно это выражение обозначает: Сначала идет строка 'abc', затем проверяется РВ '\w * 3', если оно выполняется, то затем должно идти 'xyz', в противном случае должно идти как минимум одно число, а затем 'klmn'.
Может быть, даже стоит ввести операторы типа case, while и for. Кроме того нужно ввести логические операции И, ИЛИ, НЕ, чтобы их использовать в условии. Не уверен на счет И и ИЛИ, ведь выражение "! if ('abc' && 'xyz') !" равносильно "! if ('abcxyz') !", а "! if ('abc' || 'xyz') !" — "! if (any ('abc', 'xyz') ) !". Но оператор отрицания нужен точно, чтобы определять что в данном месте не должно находиться.
Нужно ввести переменную, которая обозначает позицию в строке, где сейчас осуществляется поиск (пусть будет переменная _pos_), а так же переменная, хранящая саму строку, к которой применяется РВ (_this_). Тогда оператор "^" можно заменить более понятным "! _pos_ == 0 !", а "$" на "! _pos_ == (strlen(_this_) — 1) !" Может быть стоит ввести отдельное обозначение для конца строки, например, по аналогии с Python: _pos_ == -1. Эти же переменные позволят сделать опережающую и ретроспективную проверку.
Нужно оставить комментарии. Как они будут выглядеть это уже не важно.
Оператор присваивания должен работать в двух режимах. Первый — это проверка и присваивание переменной строки, соответствующей регулярному выражению, то, для чего сейчас используется запись вроде "(?<foo>...)": "!foo = \w\d*; !". Точку с запятой придется использовать, чтобы показать где кончается оператор присваивания.
Второй режим присваивания — сохранение регулярного выражения без его проверки. Используется для наглядности, например,
"!
foo = !\d\w*!
'abc' foo 'xyz' foo
!"
Здесь выражение !\d\w*! (обратите внимание на восклицательные знаки) используется затем по имени переменной foo.
Это основные идеи, которые появились относительно РВ. Было бы интересно попробовать такие выражения в деле, но, к сожалению, руки до реализации такого парсера у меня вряд ли дойдут. А вообще, начать можно было бы с того, что такие выражения преобразовывались к классическому виду РВ, а затем обрабатывались бы готовой библиотекой.
В завершении маленький пример для поиска URL. Возможно, там не все учел, например, считается, что доменная зона может быть только com, net, info или двухбуквенная.
"!
unicode = !% any(\d, A-F) * 2 ! // Представление Unicode в адресе.
// Переменная только создается, но не проверяется
domain = !any ('com', 'net', 'info', (a-z) * {1, 2})!
host = !any (\w, '_', unicode)!
"http://" (host '.') * {1,} domain '/' * {0, 1}
"!
Надеюсь, что нигде не ошибся, но даже если и ошибся, не страшно, главное хотелось показать суть.
В завершении еще раз скажу, что главной целью всего этого было придумать как можно повысить читаемость РВ. Разумеется, при этом объем набираемого текста увеличится, но при больших РВ оно того стоит.